mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 04:16:38 +08:00
@@ -34,6 +34,7 @@ type DownloadTask struct {
|
||||
type Subscription struct {
|
||||
Base
|
||||
UserID string `gorm:"index;size:36" json:"user_id"`
|
||||
IdentityKey string `gorm:"size:64" json:"-"`
|
||||
Name string `gorm:"size:128;not null" json:"name"`
|
||||
FeedURL string `gorm:"size:2048;not null" json:"feed_url"`
|
||||
Filter string `gorm:"size:512" json:"filter"`
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type subscriptionIdentityPayload struct {
|
||||
Name string `json:"name"`
|
||||
FeedURL string `json:"feed_url"`
|
||||
Filter string `json:"filter"`
|
||||
MediaType string `json:"media_type"`
|
||||
MediaCategory string `json:"media_category"`
|
||||
SavePath string `json:"save_path"`
|
||||
SearchMode string `json:"search_mode"`
|
||||
IMDBID string `json:"imdb_id"`
|
||||
Resolution string `json:"resolution"`
|
||||
Quality string `json:"quality"`
|
||||
Effects string `json:"effects"`
|
||||
ReleaseGroups string `json:"release_groups"`
|
||||
ExcludeWords string `json:"exclude_words"`
|
||||
MinSeeders int `json:"min_seeders"`
|
||||
MaxSeeders int `json:"max_seeders"`
|
||||
MinSizeGB float64 `json:"min_size_gb"`
|
||||
MaxSizeGB float64 `json:"max_size_gb"`
|
||||
FreeOnly bool `json:"free_only"`
|
||||
WashEnabled bool `json:"wash_enabled"`
|
||||
WashPriority string `json:"wash_priority"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
// SubscriptionIdentityKey identifies one functional subscription rule. It
|
||||
// intentionally excludes display metadata and runtime/archive state because
|
||||
// those fields are backfilled or changed automatically after creation.
|
||||
func SubscriptionIdentityKey(sub *Subscription) string {
|
||||
if sub == nil {
|
||||
return ""
|
||||
}
|
||||
payload := subscriptionIdentityPayload{
|
||||
Name: subscriptionIdentityFold(sub.Name),
|
||||
FeedURL: strings.TrimSpace(sub.FeedURL),
|
||||
Filter: strings.TrimSpace(sub.Filter),
|
||||
MediaType: subscriptionIdentityFold(sub.MediaType),
|
||||
MediaCategory: subscriptionIdentityFold(sub.MediaCategory),
|
||||
SavePath: strings.TrimSpace(sub.SavePath),
|
||||
SearchMode: subscriptionIdentityFold(sub.SearchMode),
|
||||
IMDBID: subscriptionIdentityFold(sub.IMDBID),
|
||||
Resolution: subscriptionIdentityFold(sub.Resolution),
|
||||
Quality: subscriptionIdentityFold(sub.Quality),
|
||||
Effects: subscriptionIdentityList(sub.Effects),
|
||||
ReleaseGroups: subscriptionIdentityList(sub.ReleaseGroups),
|
||||
ExcludeWords: subscriptionIdentityList(sub.ExcludeWords),
|
||||
MinSeeders: sub.MinSeeders,
|
||||
MaxSeeders: sub.MaxSeeders,
|
||||
MinSizeGB: sub.MinSizeGB,
|
||||
MaxSizeGB: sub.MaxSizeGB,
|
||||
FreeOnly: sub.FreeOnly,
|
||||
WashEnabled: sub.WashEnabled,
|
||||
WashPriority: subscriptionIdentityFold(sub.WashPriority),
|
||||
Priority: sub.Priority,
|
||||
}
|
||||
raw, _ := json.Marshal(payload)
|
||||
sum := sha256.Sum256(raw)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func RefreshSubscriptionIdentity(sub *Subscription) string {
|
||||
key := SubscriptionIdentityKey(sub)
|
||||
if sub != nil {
|
||||
sub.IdentityKey = key
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func subscriptionIdentityFold(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func subscriptionIdentityList(value string) string {
|
||||
parts := strings.Split(value, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if part = subscriptionIdentityFold(part); part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
}
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSubscriptionIdentityKeyNormalizesEquivalentRules(t *testing.T) {
|
||||
base := Subscription{
|
||||
Name: " Example Show ",
|
||||
FeedURL: "site-search://search?keyword=Example",
|
||||
Filter: " Example.*Show ",
|
||||
MediaType: "TV",
|
||||
MediaCategory: " 欧美剧 ",
|
||||
SearchMode: "KEYWORD",
|
||||
Resolution: "1080P",
|
||||
Effects: " HDR, Atmos ",
|
||||
ExcludeWords: " CAM, TS ",
|
||||
WashPriority: "Balanced",
|
||||
Priority: 50,
|
||||
}
|
||||
equivalent := base
|
||||
equivalent.Name = "example show"
|
||||
equivalent.MediaType = "tv"
|
||||
equivalent.MediaCategory = "欧美剧"
|
||||
equivalent.SearchMode = "keyword"
|
||||
equivalent.Resolution = "1080p"
|
||||
equivalent.Effects = "hdr,atmos"
|
||||
equivalent.ExcludeWords = "cam,ts"
|
||||
equivalent.WashPriority = "balanced"
|
||||
if got, want := SubscriptionIdentityKey(&equivalent), SubscriptionIdentityKey(&base); got != want {
|
||||
t.Fatalf("equivalent keys differ: %s != %s", got, want)
|
||||
}
|
||||
|
||||
differentRule := base
|
||||
differentRule.Resolution = "2160p"
|
||||
if SubscriptionIdentityKey(&differentRule) == SubscriptionIdentityKey(&base) {
|
||||
t.Fatal("different resolution should produce a different identity")
|
||||
}
|
||||
|
||||
displayOnly := base
|
||||
displayOnly.PosterURL = "https://example.test/poster.jpg"
|
||||
displayOnly.Overview = "new overview"
|
||||
displayOnly.TotalEpisodes = 24
|
||||
if SubscriptionIdentityKey(&displayOnly) != SubscriptionIdentityKey(&base) {
|
||||
t.Fatal("display/runtime metadata should not change identity")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user