diff --git a/internal/service/subscription_rules.go b/internal/service/subscription_rules.go index 8c94d7b..99731c4 100644 --- a/internal/service/subscription_rules.go +++ b/internal/service/subscription_rules.go @@ -13,10 +13,16 @@ var ( seasonOnlyRE = regexp.MustCompile(`(?i)(?:^|[\s._-])(?:s|season)\s*\d{1,2}(?:[\s._-]|$)|第\s*\d+\s*季`) ) -// defaultExcludeWords 是默认过滤的「垃圾版本」排除清单,对所有订阅生效, -// 与用户自定义排除词合并。拉丁词在 containsAnyExcludeToken 里按词边界匹配以避免子串误伤。 +// defaultExcludeWords 是默认过滤的「垃圾版本」排除清单,对所有订阅生效。 +// 拉丁词在 containsAnyExcludeToken 里按词边界匹配以避免子串误伤。 const defaultExcludeWords = "cam,ts,tc,telesync,telecine,hdcam,hdts,枪版,抢先,抢鲜,预告,trailer,sample" +// defaultCompatibilityExcludeWords 是面向自动订阅的兼容性默认排除清单。 +// 仅在用户未真正自定义排除词时启用,避免默认命中 DoVi/H.265/10bit/杜比音轨等版本。 +const defaultCompatibilityExcludeWords = "dovi,dv,dolby vision,dolby,杜比视界,杜比,h265,h.265,h-265,h_265,h 265,hevc,x265,10bit,10-bit,10 bit,hi10p,atmos,truehd,ddp,dd+,eac3" + +const legacyFrontendExcludeWords = "cam,ts,tc,枪版" + func matchesSubscriptionRules(sub *model.Subscription, title string) bool { titleFold := strings.ToLower(title) if containsAnyExcludeToken(titleFold, defaultExcludeWords) { @@ -25,6 +31,9 @@ func matchesSubscriptionRules(sub *model.Subscription, title string) bool { if sub == nil { return true } + if shouldApplyDefaultCompatibilityExcludes(sub.ExcludeWords) && containsAnyExcludeToken(titleFold, defaultCompatibilityExcludeWords) { + return false + } if sub.ExcludeWords != "" && containsAnyExcludeToken(titleFold, sub.ExcludeWords) { return false } @@ -43,6 +52,24 @@ func matchesSubscriptionRules(sub *model.Subscription, title string) bool { return true } +func shouldApplyDefaultCompatibilityExcludes(excludeWords string) bool { + normalized := normalizeExcludeWords(excludeWords) + return normalized == "" || normalized == normalizeExcludeWords(legacyFrontendExcludeWords) +} + +func normalizeExcludeWords(csv string) string { + parts := make([]string, 0) + for _, token := range strings.FieldsFunc(strings.ToLower(csv), func(r rune) bool { + return r == ',' || r == '/' || r == '|' || r == ';' || r == ',' + }) { + token = strings.TrimSpace(token) + if token != "" { + parts = append(parts, token) + } + } + return strings.Join(parts, ",") +} + func subscriptionCandidateScore(sub *model.Subscription, item SearchResult) int { title := strings.ToLower(subscriptionSearchResultText(item)) score := item.Seeders diff --git a/internal/service/subscription_rules_test.go b/internal/service/subscription_rules_test.go index b343a92..4d905ad 100644 --- a/internal/service/subscription_rules_test.go +++ b/internal/service/subscription_rules_test.go @@ -39,6 +39,45 @@ func TestMatchesSubscriptionRulesDefaultExcludesJunkReleases(t *testing.T) { } } +func TestMatchesSubscriptionRulesDefaultExcludesCompatibilityReleases(t *testing.T) { + cases := []struct { + name string + sub *model.Subscription + }{ + {name: "empty exclude words", sub: &model.Subscription{}}, + {name: "legacy frontend defaults", sub: &model.Subscription{ExcludeWords: "cam,ts,tc,枪版"}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + for _, title := range []string{ + "Some Movie 2024 2160p DoVi H.265 10bit", + "Some Movie 2024 2160p H-265", + "Some Movie 2024 1080p HEVC", + "Some Movie 2024 1080p x265", + "Some Movie 2024 2160p Dolby Vision Atmos", + "某电影 2024 1080p 杜比全景声", + "Some Anime 2024 1080p Hi10P", + } { + if matchesSubscriptionRules(c.sub, title) { + t.Errorf("expected default compatibility rules to exclude %q", title) + } + } + }) + } +} + +func TestMatchesSubscriptionRulesCustomExcludeWordsCanOptOutOfCompatibilityDefaults(t *testing.T) { + sub := &model.Subscription{ExcludeWords: "sample"} + title := "Some Movie 2024 2160p DoVi HEVC 10bit" + if !matchesSubscriptionRules(sub, title) { + t.Fatalf("custom exclude words should not force default compatibility excludes for %q", title) + } + if matchesSubscriptionRules(sub, "Some Movie 2024 1080p SAMPLE") { + t.Fatal("custom exclude words should still apply") + } +} + func TestMatchesSubscriptionRulesWordBoundaryAvoidsFalsePositives(t *testing.T) { sub := &model.Subscription{} // "ts" / "cam" / "tc" 作为子串出现在合法标题里时不应被默认排除误伤。 diff --git a/web/src/pages/SubscriptionForm.tsx b/web/src/pages/SubscriptionForm.tsx index 014b334..f3c703f 100644 --- a/web/src/pages/SubscriptionForm.tsx +++ b/web/src/pages/SubscriptionForm.tsx @@ -110,7 +110,7 @@ export function SubscriptionForm({ values, editing, onSubmit, onCancelEdit, onCh /> onChange('excludeWords', e.target.value)} /> diff --git a/web/src/pages/discoverDetailModalModel.ts b/web/src/pages/discoverDetailModalModel.ts index 98d2735..82e27ef 100644 --- a/web/src/pages/discoverDetailModalModel.ts +++ b/web/src/pages/discoverDetailModalModel.ts @@ -1,6 +1,7 @@ import type { DiscoverItem } from '../api/discover' import { buildSiteSearchFeedURL, buildSubscriptionAliases } from '../api/subscriptions' import { buildSubscribeKeyword } from './discoverPageModel' +import { defaultSubscriptionExcludeWords } from './subscriptionFormModel' export type DiscoverSubscriptionForm = { keyword: string @@ -34,7 +35,7 @@ export function initialDiscoverSubscriptionForm(item: DiscoverItem): DiscoverSub quality: '', effects: '', release_groups: '', - exclude_words: 'cam,ts,tc,枪版', + exclude_words: defaultSubscriptionExcludeWords, wash_enabled: false, wash_priority: 'balanced', save_path: '', diff --git a/web/src/pages/subscriptionFormModel.ts b/web/src/pages/subscriptionFormModel.ts index 49b9e45..da11593 100644 --- a/web/src/pages/subscriptionFormModel.ts +++ b/web/src/pages/subscriptionFormModel.ts @@ -16,6 +16,9 @@ export interface SubscriptionFormValues { washPriority: string } +export const defaultSubscriptionExcludeWords = + 'cam,ts,tc,枪版,dovi,dv,dolby vision,dolby,杜比视界,杜比,h265,h.265,h-265,h_265,hevc,x265,10bit,10-bit,hi10p,atmos,truehd,ddp,dd+,eac3' + export const defaultSubscriptionFormValues: SubscriptionFormValues = { name: '', feed: '', @@ -29,7 +32,7 @@ export const defaultSubscriptionFormValues: SubscriptionFormValues = { quality: '', effects: '', releaseGroups: '', - excludeWords: 'cam,ts,tc,枪版', + excludeWords: defaultSubscriptionExcludeWords, washEnabled: false, washPriority: 'balanced', }