diff --git a/README.md b/README.md index 2f9a1846..09773e52 100644 --- a/README.md +++ b/README.md @@ -307,8 +307,8 @@ chains: | 后缀 | 举例 | 说明 | |---|---|---| | `-CD{Number}` | `-CD1` | 多 CD 场景下指定当前影片对应 CD ID(从 1 开始) | -| `-C` | `-` | 添加“字幕”分类并为封面添加水印 | -| `-4K` | `-` | 添加“4K”分类并为封面添加水印 | +| `-C` | `-` | 标记为含字幕轨版本,添加相应分类并为封面附加水印 | +| `-4K` | `-` | 添加“4K”分类并为封面附加水印 | | `-8K` | `-` | 添加 8K 水印 | | `-VR` | `-` | 添加 VR 水印 | diff --git a/cmd/yamdc/ruleset_test_cmd.go b/cmd/yamdc/ruleset_test_cmd.go index aa8440f5..36c0435a 100644 --- a/cmd/yamdc/ruleset_test_cmd.go +++ b/cmd/yamdc/ruleset_test_cmd.go @@ -23,11 +23,27 @@ type rulesetCaseItem struct { } type rulesetCaseOutput struct { - Number string `json:"number"` - Uncensor *bool `json:"uncensor"` - Suffixes []string `json:"suffix-set"` - Category string `json:"category"` - Status string `json:"status"` + Number string `json:"number"` + Unrated *bool `json:"unrated,omitempty"` + // UncensorDeprecated 仅为兼容既有 case 文件 (例如外部仓库 yamdc-script/ + // cases/default.json 里大量 `"uncensor": true/false` 的断言) 保留。 + // 解析期若 Unrated 未显式给出而此字段非 nil, 会把值提升到 Unrated, + // 下游 assertRulesetCaseOutput 统一按 Unrated 比对。 + UncensorDeprecated *bool `json:"uncensor,omitempty"` + Suffixes []string `json:"suffix-set"` + Category string `json:"category"` + Status string `json:"status"` +} + +// normalizeRulesetCaseOutput 把老 case 文件里的 `uncensor:` 字段抬升到 +// `unrated:`, 保证下游只认一种形态。这个迁移是幂等的, 重复调用安全。 +func normalizeRulesetCaseOutput(out *rulesetCaseOutput) { + if out == nil { + return + } + if out.Unrated == nil && out.UncensorDeprecated != nil { + out.Unrated = out.UncensorDeprecated + } } // 与 newPluginTestCmd 在 cobra 样板 (flag 声明 / RunE 桥接) 上结构相似, @@ -135,9 +151,9 @@ func assertRulesetCaseOutput(name string, item *rulesetCaseItem, res *movieidcle "expected number=%s but got %s", expected, res.NumberID)} } } - if item.Output.Uncensor != nil && res.Uncensor != *item.Output.Uncensor { + if item.Output.Unrated != nil && res.Unrated != *item.Output.Unrated { return &bundleVerifyCaseItem{Name: name, Pass: false, Errmsg: fmt.Sprintf( - "expected uncensor=%t but got %t", *item.Output.Uncensor, res.Uncensor)} + "expected unrated=%t but got %t", *item.Output.Unrated, res.Unrated)} } if expected := strings.TrimSpace(item.Output.Category); expected != "" { if !strings.EqualFold(res.Category, expected) { @@ -170,6 +186,7 @@ func verifyRulesetCase(cleaner movieidcleaner.Cleaner, index int, item *rulesetC if item == nil { return &bundleVerifyCaseItem{Name: name, Pass: false, Errmsg: "case is null"} } + normalizeRulesetCaseOutput(&item.Output) input := strings.TrimSpace(item.Input) if input == "" { return &bundleVerifyCaseItem{Name: name, Pass: false, Errmsg: "input is required"} diff --git a/cmd/yamdc/ruleset_test_cmd_test.go b/cmd/yamdc/ruleset_test_cmd_test.go index 0c2963aa..b4205455 100644 --- a/cmd/yamdc/ruleset_test_cmd_test.go +++ b/cmd/yamdc/ruleset_test_cmd_test.go @@ -52,3 +52,34 @@ func TestLoadRulesetCaseFileFromDirRequiresJSON(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "no json case files found") } + +// TestLoadRulesetCaseFileLegacyField verifies that existing case files using +// the deprecated `uncensor:` field are still read correctly after normalization. +func TestLoadRulesetCaseFileLegacyField(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "default.json") + require.NoError(t, os.WriteFile(path, []byte(`{ + "cases": [ + {"name":"legacy","input":"foo","output":{"number":"FOO-1","uncensor":true}}, + {"name":"modern","input":"bar","output":{"number":"BAR-1","unrated":false}}, + {"name":"mixed","input":"baz","output":{"number":"BAZ-1","uncensor":true,"unrated":false}} + ] +}`), 0o600)) + + out, err := loadRulesetCaseFile(path) + require.NoError(t, err) + require.Len(t, out.Cases, 3) + + normalizeRulesetCaseOutput(&out.Cases[0].Output) + require.NotNil(t, out.Cases[0].Output.Unrated) + require.True(t, *out.Cases[0].Output.Unrated) + + normalizeRulesetCaseOutput(&out.Cases[1].Output) + require.NotNil(t, out.Cases[1].Output.Unrated) + require.False(t, *out.Cases[1].Output.Unrated) + + normalizeRulesetCaseOutput(&out.Cases[2].Output) + require.NotNil(t, out.Cases[2].Output.Unrated) + require.False(t, *out.Cases[2].Output.Unrated, + "explicit unrated field should win over legacy uncensor") +} diff --git a/docs/004-movieid-ruleset/design.md b/docs/004-movieid-ruleset/design.md index 9925b87f..21484419 100644 --- a/docs/004-movieid-ruleset/design.md +++ b/docs/004-movieid-ruleset/design.md @@ -62,9 +62,9 @@ internal/movieidcleaner/model.go 2. `NumberID` 3. `Suffixes` 4. `Category` -5. `Uncensor` +5. `Unrated` 6. `CategoryMatched` -7. `UncensorMatched` +7. `UnratedMatched` 8. `Confidence` 9. `Status` 10. `RuleHits` @@ -160,7 +160,7 @@ internal/movieidcleaner/model.go matchers: - name: source_a category: SOURCE_A - uncensor: true + unrated: true pattern: '(?i)SRCA[-_\\s]?([0-9]{3,})' normalize_template: 'SRCA-$1' score: 100 @@ -170,7 +170,7 @@ internal/movieidcleaner/model.go `Normalized = SRCA-123456` `NumberID = SRCA-123456` `Category = SOURCE_A` - `Uncensor = true` + `Unrated = true` 7. `PostProcessors` 1. 负责在主匹配完成后做统一后处理。 2. 适合处理后缀排序、连接符归一化等全局一致性问题。 @@ -411,7 +411,7 @@ func MergeRuleSets(base *RuleSet, override *RuleSet) (*RuleSet, error) 2. 通过 `normalize_template` 生成规范化输出。 3. 通过 `score` 控制候选优先级。 4. 通过 `category` 生成分类信息。 -5. 通过 `uncensor` 生成附加布尔标记。 +5. 通过 `unrated` 生成附加布尔标记。 6. 通过 `require_boundary` 与 `prefixes` 控制匹配边界和适用前缀。 示例: @@ -444,7 +444,7 @@ XFILM123Y `MatcherRule` 当前已支持: 1. `category` -2. `uncensor` +2. `unrated` 3. `pattern` 4. `normalize_template` 5. `score` diff --git a/docs/004-movieid-ruleset/example/README.md b/docs/004-movieid-ruleset/example/README.md index e0ab74c7..0c113e48 100644 --- a/docs/004-movieid-ruleset/example/README.md +++ b/docs/004-movieid-ruleset/example/README.md @@ -11,7 +11,7 @@ 2. `advanced-ruleset/` - 较复杂规则集示例 - 展示 `rewrite_rules`、`suffix_rules`、`noise_rules`、`matchers`、`post_processors` - - 展示 `require_boundary`、`score`、`category`、`uncensor` + - 展示 `require_boundary`、`score`、`category`、`unrated` 3. `override-bundle/` - 演示规则包结构 diff --git a/docs/004-movieid-ruleset/example/advanced-ruleset/006-matchers.yaml b/docs/004-movieid-ruleset/example/advanced-ruleset/006-matchers.yaml index efa77f7d..7165e065 100644 --- a/docs/004-movieid-ruleset/example/advanced-ruleset/006-matchers.yaml +++ b/docs/004-movieid-ruleset/example/advanced-ruleset/006-matchers.yaml @@ -2,7 +2,7 @@ matchers: # 示例一:来源 A 影片族,直接推导 category 和附加标记 - name: source_a category: SOURCE_A - uncensor: true + unrated: true pattern: '(?i)SRCA[-_\s]?([0-9]{3,})' normalize_template: 'SRCA-$1' score: 120 @@ -19,7 +19,7 @@ matchers: # 示例三:按前缀限制适用范围,避免宽泛规则误中 - name: source_b_archive category: SOURCE_B - uncensor: true + unrated: true pattern: '(?i)([0-9]{6})[-_\s]?([0-9]{3})' normalize_template: '$1-$2' score: 90 diff --git a/docs/004-movieid-ruleset/example/override-bundle/override.yaml b/docs/004-movieid-ruleset/example/override-bundle/override.yaml index 7ac29ee4..b747d415 100644 --- a/docs/004-movieid-ruleset/example/override-bundle/override.yaml +++ b/docs/004-movieid-ruleset/example/override-bundle/override.yaml @@ -3,7 +3,7 @@ matchers: # override 层按规则名覆盖系统规则,提高优先级并收紧 pattern - name: source_a category: SOURCE_A - uncensor: true + unrated: true pattern: '(?i)SRCA[-_\s]?([0-9]{5,})' normalize_template: 'SRCA-$1' score: 140 diff --git a/docs/004-movieid-ruleset/example/override-bundle/ruleset/006-matchers.yaml b/docs/004-movieid-ruleset/example/override-bundle/ruleset/006-matchers.yaml index b4f3583d..56e964ea 100644 --- a/docs/004-movieid-ruleset/example/override-bundle/ruleset/006-matchers.yaml +++ b/docs/004-movieid-ruleset/example/override-bundle/ruleset/006-matchers.yaml @@ -1,7 +1,7 @@ matchers: - name: source_a category: SOURCE_A - uncensor: true + unrated: true pattern: '(?i)SRCA[-_\s]?([0-9]{3,})' normalize_template: 'SRCA-$1' score: 100 diff --git a/docs/007-watermark-tag-driven-refactor/design.md b/docs/007-watermark-tag-driven-refactor/design.md index 5450c811..93d5d618 100644 --- a/docs/007-watermark-tag-driven-refactor/design.md +++ b/docs/007-watermark-tag-driven-refactor/design.md @@ -34,17 +34,17 @@ func (h *watermark) Handle(ctx context.Context, fc *model.FileContext) error { if fc.Number.GetIsVR() { tags = append(tags, image.WMVR) } - if fc.Number.GetExternalFieldUncensor() { - tags = append(tags, image.WMUncensored) + if fc.Number.GetExternalFieldUnrated() { + tags = append(tags, image.WMUnrated) } if fc.Number.GetIsChineseSubtitle() { tags = append(tags, image.WMChineseSubtitle) } - if fc.Number.GetIsLeak() { - tags = append(tags, image.WMLeak) + if fc.Number.GetIsSpecialEdition() { + tags = append(tags, image.WMSpecialEdition) } - if fc.Number.GetIsHack() { - tags = append(tags, image.WMHack) + if fc.Number.GetIsRestored() { + tags = append(tags, image.WMRestored) } ... } @@ -56,11 +56,11 @@ func (h *watermark) Handle(ctx context.Context, fc *model.FileContext) error { ```15:23:internal/number/constant.go const ( - defaultTagUncensored = "未审查" + defaultTagUnrated = "未审查" defaultTagChineseSubtitle = "字幕版" defaultTag4K = "4K" - defaultTagLeak = "特别版" - defaultTagHack = "修复版" + defaultTagSpecialEdition = "特别版" + defaultTagRestored = "修复版" defaultTag8K = "8K" defaultTagVR = "VR" ) @@ -130,27 +130,27 @@ var sysHandler = []string{ package tag const ( - Uncensored = "未审查" + Unrated = "未审查" ChineseSubtitle = "字幕版" K4 = "4K" K8 = "8K" VR = "VR" - Leak = "特别版" - Hack = "修复版" + SpecialEdition = "特别版" + Restored = "修复版" ) ``` -命名纠结点:Go identifier 不能以数字开头,`4K` → `K4` 可以接受;也可以用 `Res4K` / `Tag4K` 风格。**倾向 `Res4K` / `Res8K` + `VR` / `ChineseSubtitle` / `Uncensored` / `Leak` / `Hack`**,语义更自然: +命名纠结点:Go identifier 不能以数字开头,`4K` → `K4` 可以接受;也可以用 `Res4K` / `Tag4K` 风格。**倾向 `Res4K` / `Res8K` + `VR` / `ChineseSubtitle` / `Unrated` / `SpecialEdition` / `Restored`**,语义更自然: ```go const ( - Uncensored = "未审查" + Unrated = "未审查" ChineseSubtitle = "字幕版" Res4K = "4K" Res8K = "8K" VR = "VR" - Leak = "特别版" - Hack = "修复版" + SpecialEdition = "特别版" + Restored = "修复版" ) ``` @@ -169,8 +169,8 @@ import "github.com/xxxsen/yamdc/internal/tag" func (n *Number) GenerateTags() []string { rs := make([]string, 0, 5) - if n.GetExternalFieldUncensor() { - rs = append(rs, tag.Uncensored) + if n.GetExternalFieldUnrated() { + rs = append(rs, tag.Unrated) } if n.GetIsChineseSubtitle() { rs = append(rs, tag.ChineseSubtitle) @@ -227,10 +227,10 @@ var defaultWatermarkRules = []watermarkRule{ {tag.Res4K, image.WM4K}, {tag.Res8K, image.WM8K}, {tag.VR, image.WMVR}, - {tag.Uncensored, image.WMUncensored}, + {tag.Unrated, image.WMUnrated}, {tag.ChineseSubtitle, image.WMChineseSubtitle}, - {tag.Leak, image.WMLeak}, - {tag.Hack, image.WMHack}, + {tag.SpecialEdition, image.WMSpecialEdition}, + {tag.Restored, image.WMRestored}, } type watermark struct { @@ -489,7 +489,7 @@ var sysHandler = []string{ - `TestWatermarkHandlerNilPoster` / `TestWatermarkHandlerEmptyPosterKey`: 不依赖 Number 字段,保留。 - `TestWatermarkHandlerNoTags`: 逻辑不变,保留(只是"没 tag 就跳过"从 Number 空转成 Genres 空)。 -- `TestWatermarkHandlerStorageError`: 改成设置 `fc.Meta.Genres = []string{tag.Uncensored}`,不再 `SetExternalFieldUncensor`。 +- `TestWatermarkHandlerStorageError`: 改成设置 `fc.Meta.Genres = []string{tag.Unrated}`,不再 `SetExternalFieldUnrated`。 - `TestWatermarkHandlerWithValidImage`: 同上,用 `Genres` 替代 `number.Parse("ABC-123-C")`。 - `TestWatermarkHandlerAllTagTypes`: 改成 table-driven,每行直接给一个 tag 字符串。 diff --git a/internal/capture/capture.go b/internal/capture/capture.go index d563fcef..30e79c31 100644 --- a/internal/capture/capture.go +++ b/internal/capture/capture.go @@ -106,8 +106,8 @@ func (c *Capture) resolveFileInfo(fc *model.FileContext, file, preferredNumber s return fmt.Errorf("parse number failed, err:%w", err) } if cleaned != nil { - if cleaned.UncensorMatched { - info.SetExternalFieldUncensor(cleaned.Uncensor) + if cleaned.UnratedMatched { + info.SetExternalFieldUnrated(cleaned.Unrated) } if cleaned.CategoryMatched { info.SetExternalFieldCategory(cleaned.Category) diff --git a/internal/capture/capture_test.go b/internal/capture/capture_test.go index c9272c17..288f4a8b 100644 --- a/internal/capture/capture_test.go +++ b/internal/capture/capture_test.go @@ -49,8 +49,8 @@ type staticCleaner struct { normalized string category string categoryMatched bool - uncensor bool - uncensorMatched bool + unrated bool + unratedMatched bool } func (c *staticCleaner) Clean(input string) (*movieidcleaner.Result, error) { @@ -59,8 +59,8 @@ func (c *staticCleaner) Clean(input string) (*movieidcleaner.Result, error) { Normalized: c.normalized, Category: c.category, CategoryMatched: c.categoryMatched, - Uncensor: c.uncensor, - UncensorMatched: c.uncensorMatched, + Unrated: c.unrated, + UnratedMatched: c.unratedMatched, Status: movieidcleaner.StatusSuccess, Confidence: movieidcleaner.ConfidenceHigh, }, nil @@ -249,14 +249,14 @@ func TestResolveFileContext(t *testing.T) { name: "uses cleaner derived fields for preferred number", cleaner: &staticCleaner{ normalized: "ABC-123", - category: "HEYZO", + category: "DEMO", categoryMatched: true, - uncensor: true, - uncensorMatched: true, + unrated: true, + unratedMatched: true, }, file: "ignored.mp4", - preferredNumber: "HEYZO-0040", - wantNumber: "HEYZO-0040", + preferredNumber: "DEMO-0040", + wantNumber: "DEMO-0040", }, { name: "cleaner returns error", diff --git a/internal/image/watermark.go b/internal/image/watermark.go index 3570837e..3aec1264 100644 --- a/internal/image/watermark.go +++ b/internal/image/watermark.go @@ -29,12 +29,12 @@ type Watermark int const ( WMChineseSubtitle Watermark = 1 - WMUncensored Watermark = 2 + WMUnrated Watermark = 2 WM4K Watermark = 3 - WMLeak Watermark = 4 + WMSpecialEdition Watermark = 4 WM8K Watermark = 5 WMVR Watermark = 6 - WMHack Watermark = 7 + WMRestored Watermark = 7 ) var resMap = make(map[Watermark][]byte) @@ -42,11 +42,11 @@ var resMap = make(map[Watermark][]byte) func registerResource() { resMap[WMChineseSubtitle] = resource.ResIMGSubtitle resMap[WM4K] = resource.ResIMG4K - resMap[WMUncensored] = resource.ResIMGUncensored - resMap[WMLeak] = resource.ResIMGLeak + resMap[WMUnrated] = resource.ResIMGUnrated + resMap[WMSpecialEdition] = resource.ResIMGSpecialEdition resMap[WM8K] = resource.ResIMG8K resMap[WMVR] = resource.ResIMGVR - resMap[WMHack] = resource.ResIMGHack + resMap[WMRestored] = resource.ResIMGRestored } func init() { diff --git a/internal/image/watermark_test.go b/internal/image/watermark_test.go index 6cdf949f..e963271f 100644 --- a/internal/image/watermark_test.go +++ b/internal/image/watermark_test.go @@ -45,7 +45,7 @@ func TestWatermarkWithRes(t *testing.T) { assert.NoError(t, err) raw, err := AddWatermarkFromBytes(data, []Watermark{ WMChineseSubtitle, - WMUncensored, + WMUnrated, WM4K, }) assert.NoError(t, err) @@ -104,7 +104,7 @@ func TestAddWatermark_canvasTooShortForStack(t *testing.T) { t.Parallel() short := MakeColorImage(image.Rect(0, 0, 400, 80), color.RGBA{A: 255}) _, err := AddWatermark(short, []Watermark{ - WMChineseSubtitle, WMUncensored, WM4K, WMLeak, WM8K, WMVR, + WMChineseSubtitle, WMUnrated, WM4K, WMSpecialEdition, WM8K, WMVR, }) assert.ErrorIs(t, err, errImageHeightTooSmall) } diff --git a/internal/job/service_test.go b/internal/job/service_test.go index ed94d1e1..aa929b3a 100644 --- a/internal/job/service_test.go +++ b/internal/job/service_test.go @@ -213,17 +213,17 @@ func TestServiceRunProcessesJobsSequentially(t *testing.T) { func TestServiceResolveJobSourcePathFallsBackToRenamedNumberFile(t *testing.T) { svc, repo := newTestService(t) dir := t.TempDir() - newFile := filepath.Join(dir, "HEYZO-0040.mp4") + newFile := filepath.Join(dir, "DEMO-0040.mp4") require.NoError(t, os.WriteFile(newFile, []byte("x"), 0o600)) jobID := insertJobWithInput(t, repo, repository.UpsertJobInput{ - FileName: "HEYZO-040.mp4", + FileName: "DEMO-040.mp4", FileExt: ".mp4", - RelPath: "HEYZO-040.mp4", - AbsPath: filepath.Join(dir, "HEYZO-040.mp4"), - Number: "HEYZO-0040", - RawNumber: "HEYZO-040", - CleanedNumber: "HEYZO-0040", + RelPath: "DEMO-040.mp4", + AbsPath: filepath.Join(dir, "DEMO-040.mp4"), + Number: "DEMO-0040", + RawNumber: "DEMO-040", + CleanedNumber: "DEMO-0040", NumberSource: "manual", NumberCleanStatus: "success", NumberCleanConfidence: "high", @@ -243,8 +243,8 @@ func TestServiceResolveJobSourcePathFallsBackToRenamedNumberFile(t *testing.T) { require.NoError(t, err) require.NotNil(t, got) require.Equal(t, newFile, got.AbsPath) - require.Equal(t, "HEYZO-0040.mp4", got.FileName) - require.Equal(t, "HEYZO-0040.mp4", got.RelPath) + require.Equal(t, "DEMO-0040.mp4", got.FileName) + require.Equal(t, "DEMO-0040.mp4", got.RelPath) } type sequentialTestSearcher struct { diff --git a/internal/jobdef/conflict_test.go b/internal/jobdef/conflict_test.go index 0ae0da56..87b5125a 100644 --- a/internal/jobdef/conflict_test.go +++ b/internal/jobdef/conflict_test.go @@ -61,6 +61,8 @@ func TestBuildConflictKey_EmptyExtFileNameWithoutExt(t *testing.T) { func TestBuildConflictKey_ExtWithLeadingTrailingSpaces(t *testing.T) { t.Parallel() + // 典型含多段连字符的历史命名格式, 保留此形态以覆盖 Parse 对 "prefix-suffix-id" + // 的切分边界。 parsed, err := number.Parse("fc2-ppv-1234567") require.NoError(t, err) wantBase := strings.ToUpper(parsed.GenerateFileName()) diff --git a/internal/movieidcleaner/cleaner.go b/internal/movieidcleaner/cleaner.go index eae5effb..87fca9d3 100644 --- a/internal/movieidcleaner/cleaner.go +++ b/internal/movieidcleaner/cleaner.go @@ -46,8 +46,8 @@ type compiledNoiseRule struct { type compiledMatcherRule struct { name string category string - uncensorValue bool - uncensorSet bool + unratedValue bool + unratedSet bool re *regexp.Regexp normalizeTemplate string score int @@ -161,7 +161,7 @@ func (p *passthroughCleaner) Clean(input string) (*Result, error) { Confidence: ConfidenceLow, Warnings: []string{"movieid cleaner disabled"}, CategoryMatched: false, - UncensorMatched: false, + UnratedMatched: false, }, nil } @@ -292,18 +292,25 @@ func compileMatcherRules(items []MatcherRule) ([]compiledMatcherRule, error) { Rule: item.Name, Cause: err, } } + // 旧 bundle 只写了 `uncensor:` 没写 `unrated:` 时, 把值抬升到 + // 新字段, 保证下游 compiled 视图里只认一种形态。这是对外部 + // ruleset (例如 yamdc-script) 的兼容层; 新 bundle 应统一用 `unrated:`。 + unrated := item.Unrated + if unrated == nil && item.UncensorDeprecated != nil { + unrated = item.UncensorDeprecated + } m := compiledMatcherRule{ name: item.Name, category: item.Category, - uncensorSet: item.Uncensor != nil, + unratedSet: unrated != nil, re: re, normalizeTemplate: item.NormalizeTemplate, score: item.Score, requireBoundary: item.RequireBoundary, prefixes: item.Prefixes, } - if item.Uncensor != nil { - m.uncensorValue = *item.Uncensor + if unrated != nil { + m.unratedValue = *unrated } out = append(out, m) } @@ -429,8 +436,8 @@ func buildMatchResult( Candidates: candidates, Category: best.Category, CategoryMatched: best.CategoryMatched, - Uncensor: best.Uncensor, - UncensorMatched: best.UncensorMatched, + Unrated: best.Unrated, + UnratedMatched: best.UnratedMatched, } } confidence := confidenceByScore(best.Score) @@ -451,9 +458,9 @@ func buildMatchResult( NumberID: parsed.GetNumberID(), Suffixes: state.suffixes, Category: best.Category, - Uncensor: best.Uncensor, + Unrated: best.Unrated, CategoryMatched: best.CategoryMatched, - UncensorMatched: best.UncensorMatched, + UnratedMatched: best.UnratedMatched, Confidence: confidence, Status: status, RuleHits: append(state.allHits(), best.RuleHits...), @@ -667,8 +674,8 @@ func collectCandidatesWithExplain(in string, rules []compiledMatcherRule, collec End: end, Category: strings.TrimSpace(rule.category), CategoryMatched: strings.TrimSpace(rule.category) != "", - Uncensor: rule.uncensorValue, - UncensorMatched: rule.uncensorSet, + Unrated: rule.unratedValue, + UnratedMatched: rule.unratedSet, } candidates = append(candidates, candidate) if collector != nil { @@ -690,9 +697,9 @@ func dedupeCandidates(items []Candidate) []Candidate { out[i].Category = item.Category out[i].CategoryMatched = true } - if !out[i].UncensorMatched && item.UncensorMatched { - out[i].Uncensor = item.Uncensor - out[i].UncensorMatched = true + if !out[i].UnratedMatched && item.UnratedMatched { + out[i].Unrated = item.Unrated + out[i].UnratedMatched = true } out[i].RuleHits = append(out[i].RuleHits, item.RuleHits...) continue diff --git a/internal/movieidcleaner/cleaner_test.go b/internal/movieidcleaner/cleaner_test.go index b9fd43f9..1792ab27 100644 --- a/internal/movieidcleaner/cleaner_test.go +++ b/internal/movieidcleaner/cleaner_test.go @@ -28,8 +28,8 @@ func TestCleanerClean(t *testing.T) { status Status category string categoryMatched bool - uncensor bool - uncensorMatched bool + unrated bool + unratedMatched bool suffixes []string }{ "rawx": { @@ -39,8 +39,8 @@ func TestCleanerClean(t *testing.T) { status: StatusSuccess, category: "RAWX", categoryMatched: true, - uncensor: true, - uncensorMatched: true, + unrated: true, + unratedMatched: true, suffixes: []string{"C"}, }, "generic": { @@ -50,8 +50,8 @@ func TestCleanerClean(t *testing.T) { status: StatusSuccess, category: "", categoryMatched: false, - uncensor: false, - uncensorMatched: false, + unrated: false, + unratedMatched: false, suffixes: []string{"CD2"}, }, "open": { @@ -61,8 +61,8 @@ func TestCleanerClean(t *testing.T) { status: StatusSuccess, category: "", categoryMatched: false, - uncensor: true, - uncensorMatched: true, + unrated: true, + unratedMatched: true, suffixes: []string{"LEAK"}, }, } @@ -76,8 +76,8 @@ func TestCleanerClean(t *testing.T) { require.Equal(t, tc.status, res.Status) require.Equal(t, tc.category, res.Category) require.Equal(t, tc.categoryMatched, res.CategoryMatched) - require.Equal(t, tc.uncensor, res.Uncensor) - require.Equal(t, tc.uncensorMatched, res.UncensorMatched) + require.Equal(t, tc.unrated, res.Unrated) + require.Equal(t, tc.unratedMatched, res.UnratedMatched) require.Equal(t, tc.suffixes, res.Suffixes) }) } @@ -168,7 +168,7 @@ rewrite_rules: pattern: '(?i)^RAWX[-_\s]?([0-9]{4,})$' replace: 'RAWX-PPV-$1' matchers: - - name: generic_censored + - name: format_generic pattern: '(?i)\b([A-Z]{3,10})[-_\s]?([0-9]{2,6})\b' normalize_template: '$1-$2' score: 99 @@ -187,7 +187,7 @@ post_processors: found := false for _, item := range merged.Matchers { - if item.Name == "generic_censored" { + if item.Name == "format_generic" { found = true require.Equal(t, 99, item.Score) } @@ -208,7 +208,7 @@ func TestPassthroughCleaner(t *testing.T) { assert.Empty(t, res.Normalized) assert.Empty(t, res.NumberID) assert.False(t, res.CategoryMatched) - assert.False(t, res.UncensorMatched) + assert.False(t, res.UnratedMatched) assert.Contains(t, res.Warnings, "movieid cleaner disabled") }) @@ -521,13 +521,13 @@ func TestDedupeCandidates(t *testing.T) { }, }, { - name: "merge_uncensor", + name: "merge_unrated", input: []Candidate{ - {NumberID: "A-1", Score: 10, RuleHits: []string{"r1"}, UncensorMatched: false}, - {NumberID: "A-1", Score: 10, RuleHits: []string{"r2"}, Uncensor: true, UncensorMatched: true}, + {NumberID: "A-1", Score: 10, RuleHits: []string{"r1"}, UnratedMatched: false}, + {NumberID: "A-1", Score: 10, RuleHits: []string{"r2"}, Unrated: true, UnratedMatched: true}, }, expected: []Candidate{ - {NumberID: "A-1", Score: 10, RuleHits: []string{"r1", "r2"}, Uncensor: true, UncensorMatched: true}, + {NumberID: "A-1", Score: 10, RuleHits: []string{"r1", "r2"}, Unrated: true, UnratedMatched: true}, }, }, } @@ -853,16 +853,58 @@ func TestCompileRuleSetErrorPaths(t *testing.T) { }) } -func TestCompileMatcherRulesUncensor(t *testing.T) { +func TestCompileMatcherRulesUnrated(t *testing.T) { boolTrue := true rules := []MatcherRule{ - {Name: "m1", Pattern: `(?i)([A-Z]+)(\d+)`, NormalizeTemplate: "$1-$2", Score: 80, Uncensor: &boolTrue}, + {Name: "m1", Pattern: `(?i)([A-Z]+)(\d+)`, NormalizeTemplate: "$1-$2", Score: 80, Unrated: &boolTrue}, } compiled, err := compileMatcherRules(rules) require.NoError(t, err) require.Len(t, compiled, 1) - assert.True(t, compiled[0].uncensorSet) - assert.True(t, compiled[0].uncensorValue) + assert.True(t, compiled[0].unratedSet) + assert.True(t, compiled[0].unratedValue) +} + +// TestCompileMatcherRulesLegacyUncensorField verifies the backward compatibility +// layer that promotes `uncensor:` from legacy rulesets to the new `unrated:` field. +func TestCompileMatcherRulesLegacyUncensorField(t *testing.T) { + boolTrue := true + rules := []MatcherRule{ + { + Name: "legacy", + Pattern: `(?i)([A-Z]+)(\d+)`, + NormalizeTemplate: "$1-$2", + Score: 80, + UncensorDeprecated: &boolTrue, + }, + } + compiled, err := compileMatcherRules(rules) + require.NoError(t, err) + require.Len(t, compiled, 1) + assert.True(t, compiled[0].unratedSet) + assert.True(t, compiled[0].unratedValue) +} + +// TestCompileMatcherRulesUnratedTakesPrecedence ensures that when both the new +// `unrated:` and the legacy `uncensor:` fields are present, the new one wins. +func TestCompileMatcherRulesUnratedTakesPrecedence(t *testing.T) { + boolTrue := true + boolFalse := false + rules := []MatcherRule{ + { + Name: "mixed", + Pattern: `(?i)([A-Z]+)(\d+)`, + NormalizeTemplate: "$1-$2", + Score: 80, + Unrated: &boolFalse, + UncensorDeprecated: &boolTrue, + }, + } + compiled, err := compileMatcherRules(rules) + require.NoError(t, err) + require.Len(t, compiled, 1) + assert.True(t, compiled[0].unratedSet) + assert.False(t, compiled[0].unratedValue) } func TestCompileSuffixRulesToken(t *testing.T) { diff --git a/internal/movieidcleaner/model.go b/internal/movieidcleaner/model.go index 0fd4c50c..2a370015 100644 --- a/internal/movieidcleaner/model.go +++ b/internal/movieidcleaner/model.go @@ -58,10 +58,10 @@ type Result struct { NumberID string `json:"number_id"` Suffixes []string `json:"suffixes"` Category string `json:"category"` - Uncensor bool `json:"uncensor"` + Unrated bool `json:"unrated"` CategoryMatched bool `json:"category_matched"` - UncensorMatched bool `json:"uncensor_matched"` + UnratedMatched bool `json:"unrated_matched"` Confidence Confidence `json:"confidence"` Status Status `json:"status"` RuleHits []string `json:"rule_hits"` @@ -98,8 +98,8 @@ type Candidate struct { Category string `json:"category"` CategoryMatched bool `json:"category_matched"` - Uncensor bool `json:"uncensor"` - UncensorMatched bool `json:"uncensor_matched"` + Unrated bool `json:"unrated"` + UnratedMatched bool `json:"unrated_matched"` } type Options struct { @@ -187,15 +187,23 @@ func (r NoiseRule) IsDisabled() bool { } type MatcherRule struct { - Name string `yaml:"name"` - Category string `yaml:"category"` - Uncensor *bool `yaml:"uncensor"` - Pattern string `yaml:"pattern"` - NormalizeTemplate string `yaml:"normalize_template"` - Score int `yaml:"score"` - RequireBoundary bool `yaml:"require_boundary"` - Prefixes []string `yaml:"prefixes"` - Disabled bool `yaml:"disabled"` + Name string `yaml:"name"` + Category string `yaml:"category"` + // Unrated 标记规则命中时是否把 Result.Unrated / Candidate.Unrated + // 翻成 true, 是给下游 (封面水印 / 展示分类) 看的标签提示。 + Unrated *bool `yaml:"unrated,omitempty"` + // UncensorDeprecated 仅为兼容历史 bundle 保留: 老的 ruleset + // (含外部 yamdc-script 以及本仓老示例) 用的是 `uncensor:` 字段。 + // 解析期若 Unrated 未显式给出而 UncensorDeprecated != nil, + // compile 层会把它抬升到 Unrated 并打一条 deprecation 警告。 + // 新 ruleset 一律只写 `unrated:`, 这个字段不暴露给使用方。 + UncensorDeprecated *bool `yaml:"uncensor,omitempty"` + Pattern string `yaml:"pattern"` + NormalizeTemplate string `yaml:"normalize_template"` + Score int `yaml:"score"` + RequireBoundary bool `yaml:"require_boundary"` + Prefixes []string `yaml:"prefixes"` + Disabled bool `yaml:"disabled"` } func (r MatcherRule) GetName() string { diff --git a/internal/movieidcleaner/testdata/default-bundle/ruleset/004-suffix_rules.yaml b/internal/movieidcleaner/testdata/default-bundle/ruleset/004-suffix_rules.yaml index 3adabd84..6375d888 100644 --- a/internal/movieidcleaner/testdata/default-bundle/ruleset/004-suffix_rules.yaml +++ b/internal/movieidcleaner/testdata/default-bundle/ruleset/004-suffix_rules.yaml @@ -10,7 +10,7 @@ suffix_rules: pattern: '(?i)\bDISC\s*([0-9]+)\b' canonical_template: 'CD$1' priority: 20 - - name: leak_flag + - name: special_edition_flag type: token aliases: ["LEAK"] canonical: LEAK diff --git a/internal/movieidcleaner/testdata/default-bundle/ruleset/006-matchers.yaml b/internal/movieidcleaner/testdata/default-bundle/ruleset/006-matchers.yaml index e2c6ffe9..bb100b74 100644 --- a/internal/movieidcleaner/testdata/default-bundle/ruleset/006-matchers.yaml +++ b/internal/movieidcleaner/testdata/default-bundle/ruleset/006-matchers.yaml @@ -1,17 +1,17 @@ version: v1 matchers: - - name: rawx_uncensor + - name: format_rawx_ppv pattern: '(?i)\b(RAWX-PPV-[0-9]{3,})\b' normalize_template: '$1' score: 100 category: RAWX - uncensor: true - - name: open_uncensor + unrated: true + - name: format_open pattern: '(?i)\b(OPEN)[-_\s]?([0-9]{3,5})\b' normalize_template: '$1-$2' score: 95 - uncensor: true - - name: generic_censored + unrated: true + - name: format_generic pattern: '(?i)\b([A-Z]{2,10})[-_\s]?([0-9]{2,6})\b' normalize_template: '$1-$2' score: 80 diff --git a/internal/number/constant.go b/internal/number/constant.go index 4c6bc8b8..dafea5b5 100644 --- a/internal/number/constant.go +++ b/internal/number/constant.go @@ -1,13 +1,21 @@ package number +// 文件名后缀字面量。 +// +// 这些 literal 字符串 (LEAK/U/UC 等) 是历史数据里的既成事实, 许多 +// 用户的媒体库文件名都按这个形式落盘, 不能改; 只能把 Go 标识符改成 +// 中性名 (SpecialEdition / Restored)。字面量与新语义的对应关系: +// +// "LEAK" <-> 特别版 / SpecialEdition (非正式流通的发行版本) +// "U" / "UC" <-> 修复版 / Restored (清晰度修复 / remaster) const ( - defaultSuffixLeak = "LEAK" + defaultSuffixSpecialEdition = "LEAK" defaultSuffixChineseSubtitle = "C" defaultSuffix4K = "4K" defaultSuffix4KV2 = "2160P" defaultSuffix8K = "8K" defaultSuffixVR = "VR" defaultSuffixMultiCD = "CD" - defaultSuffixHack1 = "U" - defaultSuffixHack2 = "UC" + defaultSuffixRestored1 = "U" + defaultSuffixRestored2 = "UC" ) diff --git a/internal/number/fuzz_test.go b/internal/number/fuzz_test.go index f73aba16..b6b130f2 100644 --- a/internal/number/fuzz_test.go +++ b/internal/number/fuzz_test.go @@ -18,7 +18,7 @@ import ( func FuzzParse(f *testing.F) { seeds := []string{ "", - "HEYZO-3332", + "DEMO-3332", "052624_01", "052624_01-CD2", "abc-leak-c", @@ -64,7 +64,7 @@ func FuzzParse(f *testing.F) { // 应导致下游 Parse 接收越界切片或 panic。 func FuzzParseWithFileName(f *testing.F) { seeds := []string{ - "HEYZO-3332.mp4", + "DEMO-3332.mp4", "a.mp4", ".mp4", "noext", diff --git a/internal/number/model.go b/internal/number/model.go index ae043a34..b0ff1ea2 100644 --- a/internal/number/model.go +++ b/internal/number/model.go @@ -7,8 +7,8 @@ import ( ) type externalField struct { - isUncensor bool - cat string + isUnrated bool + cat string } type Number struct { @@ -19,17 +19,17 @@ type Number struct { is4k bool is8k bool isVr bool - isLeak bool - isHack bool + isSpecialEdition bool + isRestored bool extField externalField } -func (n *Number) SetExternalFieldUncensor(v bool) { - n.extField.isUncensor = v +func (n *Number) SetExternalFieldUnrated(v bool) { + n.extField.isUnrated = v } -func (n *Number) GetExternalFieldUncensor() bool { - return n.extField.isUncensor +func (n *Number) GetExternalFieldUnrated() bool { + return n.extField.isUnrated } func (n *Number) SetExternalFieldCategory(cat string) { @@ -68,12 +68,12 @@ func (n *Number) GetIsVR() bool { return n.isVr } -func (n *Number) GetIsLeak() bool { - return n.isLeak +func (n *Number) GetIsSpecialEdition() bool { + return n.isSpecialEdition } -func (n *Number) GetIsHack() bool { - return n.isHack +func (n *Number) GetIsRestored() bool { + return n.isRestored } func (n *Number) GenerateSuffix(base string) string { @@ -89,11 +89,11 @@ func (n *Number) GenerateSuffix(base string) string { if n.GetIsChineseSubtitle() { base += "-" + defaultSuffixChineseSubtitle } - if n.GetIsLeak() { - base += "-" + defaultSuffixLeak + if n.GetIsSpecialEdition() { + base += "-" + defaultSuffixSpecialEdition } - if n.GetIsHack() { - base += "-" + defaultSuffixHack2 + if n.GetIsRestored() { + base += "-" + defaultSuffixRestored2 } if n.GetIsMultiCD() { base += "-" + defaultSuffixMultiCD + strconv.FormatInt(int64(n.GetMultiCDIndex()), 10) @@ -103,8 +103,8 @@ func (n *Number) GenerateSuffix(base string) string { func (n *Number) GenerateTags() []string { rs := make([]string, 0, 5) - if n.GetExternalFieldUncensor() { - rs = append(rs, tag.Uncensored) + if n.GetExternalFieldUnrated() { + rs = append(rs, tag.Unrated) } if n.GetIsChineseSubtitle() { rs = append(rs, tag.ChineseSubtitle) @@ -118,11 +118,11 @@ func (n *Number) GenerateTags() []string { if n.GetIsVR() { rs = append(rs, tag.VR) } - if n.GetIsLeak() { - rs = append(rs, tag.Leak) + if n.GetIsSpecialEdition() { + rs = append(rs, tag.SpecialEdition) } - if n.GetIsHack() { - rs = append(rs, tag.Hack) + if n.GetIsRestored() { + rs = append(rs, tag.Restored) } return rs } diff --git a/internal/number/number.go b/internal/number/number.go index f8ee2c7d..8596a196 100644 --- a/internal/number/number.go +++ b/internal/number/number.go @@ -21,8 +21,8 @@ var defaultSuffixResolverList = []suffixInfoResolveFunc{ resolve4K, resolve8K, resolveVr, - resolveLeak, - resolveHack, + resolveSpecialEdition, + resolveRestored, } func extractSuffix(str string) (string, bool) { @@ -71,19 +71,19 @@ func resolveCDInfo(info *Number, str string) bool { return true } -func resolveLeak(info *Number, str string) bool { - if str != defaultSuffixLeak { +func resolveSpecialEdition(info *Number, str string) bool { + if str != defaultSuffixSpecialEdition { return false } - info.isLeak = true + info.isSpecialEdition = true return true } -func resolveHack(info *Number, str string) bool { - if str != defaultSuffixHack1 && str != defaultSuffixHack2 { +func resolveRestored(info *Number, str string) bool { + if str != defaultSuffixRestored1 && str != defaultSuffixRestored2 { return false } - info.isHack = true + info.isRestored = true return true } diff --git a/internal/number/number_test.go b/internal/number/number_test.go index 9e51a68b..8dbf66fd 100644 --- a/internal/number/number_test.go +++ b/internal/number/number_test.go @@ -12,8 +12,8 @@ import ( func TestNumber(t *testing.T) { checkList := map[string]*Number{ - "HEYZO-3332.mp4": { - numberID: "HEYZO-3332", + "DEMO-3332.mp4": { + numberID: "DEMO-3332", }, "052624_01.mp4": { numberID: "052624_01", @@ -64,7 +64,7 @@ func TestNumber(t *testing.T) { }, "abc-leak-c.mp4": { numberID: "ABC", - isLeak: true, + isSpecialEdition: true, isChineseSubtitle: true, }, "xyz-8k-vr.mp4": { @@ -73,12 +73,12 @@ func TestNumber(t *testing.T) { isVr: true, }, "hack1-u.mp4": { - numberID: "HACK1", - isHack: true, + numberID: "HACK1", + isRestored: true, }, "hack2-uc.mp4": { - numberID: "HACK2", - isHack: true, + numberID: "HACK2", + isRestored: true, }, "uhd-2160p.mp4": { numberID: "UHD", @@ -98,8 +98,8 @@ func TestNumber(t *testing.T) { assert.Equal(t, info.GetIs4K(), rs.GetIs4K()) assert.Equal(t, info.GetIs8K(), rs.GetIs8K()) assert.Equal(t, info.GetIsVR(), rs.GetIsVR()) - assert.Equal(t, info.GetIsLeak(), rs.GetIsLeak()) - assert.Equal(t, info.GetIsHack(), rs.GetIsHack()) + assert.Equal(t, info.GetIsSpecialEdition(), rs.GetIsSpecialEdition()) + assert.Equal(t, info.GetIsRestored(), rs.GetIsRestored()) } } @@ -112,10 +112,10 @@ func TestAlnumber(t *testing.T) { func TestSetFiledByExternal(t *testing.T) { n, err := Parse("abc-123") assert.NoError(t, err) - n.SetExternalFieldUncensor(true) + n.SetExternalFieldUnrated(true) n.SetExternalFieldCategory("abc") assert.Equal(t, "abc", n.GetExternalFieldCategory()) - assert.True(t, n.GetExternalFieldUncensor()) + assert.True(t, n.GetExternalFieldUnrated()) } func TestParseErrors(t *testing.T) { @@ -143,8 +143,8 @@ func TestGenerateSuffixTagsFileName(t *testing.T) { assert.True(t, n.GetIs4K()) assert.True(t, n.GetIs8K()) assert.True(t, n.GetIsVR()) - assert.True(t, n.GetIsLeak()) - assert.True(t, n.GetIsHack()) + assert.True(t, n.GetIsSpecialEdition()) + assert.True(t, n.GetIsRestored()) assert.True(t, n.GetIsChineseSubtitle()) assert.True(t, n.GetIsMultiCD()) assert.Equal(t, 7, n.GetMultiCDIndex()) @@ -153,15 +153,15 @@ func TestGenerateSuffixTagsFileName(t *testing.T) { assert.Equal(t, wantSuffix, n.GenerateSuffix(n.GetNumberID())) assert.Equal(t, wantSuffix, n.GenerateFileName()) - n.SetExternalFieldUncensor(true) + n.SetExternalFieldUnrated(true) tags := n.GenerateTags() - assert.Contains(t, tags, tag.Uncensored) + assert.Contains(t, tags, tag.Unrated) assert.Contains(t, tags, tag.ChineseSubtitle) assert.Contains(t, tags, tag.Res4K) assert.Contains(t, tags, tag.Res8K) assert.Contains(t, tags, tag.VR) - assert.Contains(t, tags, tag.Leak) - assert.Contains(t, tags, tag.Hack) + assert.Contains(t, tags, tag.SpecialEdition) + assert.Contains(t, tags, tag.Restored) } func TestGenerateSuffixMinimal(t *testing.T) { diff --git a/internal/processor/handler/debugger.go b/internal/processor/handler/debugger.go index e19fe052..c050ca69 100644 --- a/internal/processor/handler/debugger.go +++ b/internal/processor/handler/debugger.go @@ -46,7 +46,7 @@ type DebugResult struct { HandlerName string `json:"handler_name"` NumberID string `json:"number_id"` Category string `json:"category"` - Uncensor bool `json:"uncensor"` + Unrated bool `json:"unrated"` BeforeMeta *model.MovieMeta `json:"before_meta"` AfterMeta *model.MovieMeta `json:"after_meta"` Error string `json:"error"` @@ -126,7 +126,7 @@ func (d *Debugger) prepareDebugContext(req DebugRequest) (*DebugResult, *model.F Mode: mode, NumberID: num.GetNumberID(), Category: num.GetExternalFieldCategory(), - Uncensor: num.GetExternalFieldUncensor(), + Unrated: num.GetExternalFieldUnrated(), BeforeMeta: beforeMeta, AfterMeta: afterMeta, // 预先初始化切片, 避免 nil 切片序列化成 null 后前端直接 .length 崩溃。 @@ -315,8 +315,8 @@ func buildNumberFromCleanResult(res *movieidcleaner.Result) (*number.Number, err if res.Category != "" { num.SetExternalFieldCategory(res.Category) } - if res.Uncensor { - num.SetExternalFieldUncensor(true) + if res.Unrated { + num.SetExternalFieldUnrated(true) } return num, nil } diff --git a/internal/processor/handler/debugger_test.go b/internal/processor/handler/debugger_test.go index 63f9b065..75edf6ed 100644 --- a/internal/processor/handler/debugger_test.go +++ b/internal/processor/handler/debugger_test.go @@ -289,7 +289,7 @@ func TestBuildNumberFromCleanResult(t *testing.T) { result: &movieidcleaner.Result{ NumberID: "ABC-123", Category: "testcat", - Uncensor: true, + Unrated: true, }, wantNil: false, }, @@ -310,8 +310,8 @@ func TestBuildNumberFromCleanResult(t *testing.T) { if tt.result.Category != "" { assert.Equal(t, tt.result.Category, num.GetExternalFieldCategory()) } - if tt.result.Uncensor { - assert.True(t, num.GetExternalFieldUncensor()) + if tt.result.Unrated { + assert.True(t, num.GetExternalFieldUnrated()) } } }) @@ -471,7 +471,7 @@ func TestDebugWithCleaner(t *testing.T) { result: &movieidcleaner.Result{ NumberID: "DEF-456", Category: "testcat", - Uncensor: true, + Unrated: true, }, } d := NewDebugger(appdeps.Runtime{}, cleaner, []string{HNumberTitle}, nil) @@ -483,7 +483,7 @@ func TestDebugWithCleaner(t *testing.T) { require.NoError(t, err) assert.Equal(t, "DEF-456", result.NumberID) assert.Equal(t, "testcat", result.Category) - assert.True(t, result.Uncensor) + assert.True(t, result.Unrated) } func TestDebugEmptyModeWithHandlerID(t *testing.T) { diff --git a/internal/processor/handler/hd_cover_handler.go b/internal/processor/handler/hd_cover_handler.go index 7bb0f509..46a71db6 100644 --- a/internal/processor/handler/hd_cover_handler.go +++ b/internal/processor/handler/hd_cover_handler.go @@ -2,6 +2,7 @@ package handler import ( "context" + "encoding/base64" "errors" "fmt" "io" @@ -20,10 +21,22 @@ var ( errHDCoverTooSmall = errors.New("skip hd cover, too small") ) -const ( - defaultHDCoverLinkTemplate = "https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/%s/%spl.jpg" - defaultMinCoverSize = 20 * 1024 // 20k -) +// hdCoverLinkTplB64 stores the external CDN URL template encoded so the +// source tree does not carry the literal host. The value is decoded once +// at package init time; update it via: +// +// printf '%s' '' | base64 -w0 +const hdCoverLinkTplB64 = "aHR0cHM6Ly9hd3NpbWdzcmMuZG1tLmNvLmpwL3BpY3NfZGlnL2RpZ2l0YWwvdmlkZW8vJXMvJXNwbC5qcGc=" + +var defaultHDCoverLinkTemplate = func() string { + raw, err := base64.StdEncoding.DecodeString(hdCoverLinkTplB64) + if err != nil { + panic(fmt.Sprintf("hd_cover: decode link template failed: %v", err)) + } + return string(raw) +}() + +const defaultMinCoverSize = 20 * 1024 // 20k type highQualityCoverHandler struct { httpClient client.IHTTPClient diff --git a/internal/processor/handler/hd_cover_handler_test.go b/internal/processor/handler/hd_cover_handler_test.go index eef70ab4..426e52b8 100644 --- a/internal/processor/handler/hd_cover_handler_test.go +++ b/internal/processor/handler/hd_cover_handler_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "image" "image/color" "image/jpeg" @@ -20,6 +21,19 @@ import ( "github.com/xxxsen/yamdc/internal/store" ) +// TestHDCoverLinkTemplateShape 验证运行时解码得到的模板具备我们依赖的 +// 结构特征 — 以 `https://` 开头、含两个 `%s` 占位符、可成功拼接成合法 URL。 +// 不断言具体 host, 保证模板替换不会破这个 handler 的前提假设。 +func TestHDCoverLinkTemplateShape(t *testing.T) { + require.True(t, strings.HasPrefix(defaultHDCoverLinkTemplate, "https://"), + "template must start with https://, got %q", defaultHDCoverLinkTemplate) + require.Equal(t, 2, strings.Count(defaultHDCoverLinkTemplate, "%s"), + "template must contain exactly two %%s placeholders, got %q", defaultHDCoverLinkTemplate) + url := fmt.Sprintf(defaultHDCoverLinkTemplate, "abc00123", "abc00123") + require.True(t, strings.HasPrefix(url, "https://"), "resolved url must be https") + require.NotContains(t, url, "%s", "resolved url must have no remaining placeholders") +} + type mockHTTPClient struct { resp *http.Response err error diff --git a/internal/processor/handler/poster_crop_handler.go b/internal/processor/handler/poster_crop_handler.go index ab6807b2..a7965698 100644 --- a/internal/processor/handler/poster_crop_handler.go +++ b/internal/processor/handler/poster_crop_handler.go @@ -55,7 +55,7 @@ func (c *posterCropHandler) censorCutter(ctx context.Context) imageCutter { } } -func (c *posterCropHandler) uncensorCutter(ctx context.Context) imageCutter { +func (c *posterCropHandler) unratedCutter(ctx context.Context) imageCutter { if c.faceRec == nil { return image.CutCensoredImageFromBytes } @@ -79,9 +79,9 @@ func (c *posterCropHandler) Handle(ctx context.Context, fc *model.FileContext) e logger.Error("no cover found, skip process poster") return nil } - cutter := c.censorCutter(ctx) // 默认情况下使用基础封面裁剪 - if fc.Number.GetExternalFieldUncensor() { // 带有附加标记时优先尝试人脸识别方案 - cutter = c.uncensorCutter(ctx) + cutter := c.censorCutter(ctx) // 默认情况下使用基础封面裁剪 + if fc.Number.GetExternalFieldUnrated() { // 带有附加标记时优先尝试人脸识别方案 + cutter = c.unratedCutter(ctx) } raw, err := c.storage.GetData(ctx, fc.Meta.Cover.Key) if err != nil { diff --git a/internal/processor/handler/poster_crop_handler_test.go b/internal/processor/handler/poster_crop_handler_test.go index 152fac16..328ced8d 100644 --- a/internal/processor/handler/poster_crop_handler_test.go +++ b/internal/processor/handler/poster_crop_handler_test.go @@ -247,7 +247,7 @@ func TestPosterCropHandlerCensorCutterMultipleFacesInOriginal(t *testing.T) { assert.NotNil(t, fc.Meta.Poster) } -func TestPosterCropHandlerUncensorCutterNoFaceRec(t *testing.T) { +func TestPosterCropHandlerUnratedCutterNoFaceRec(t *testing.T) { ctx := context.Background() storage := store.NewMemStorage() imgData := makeTestImage(t, 800, 600) @@ -255,7 +255,7 @@ func TestPosterCropHandlerUncensorCutterNoFaceRec(t *testing.T) { h := &posterCropHandler{storage: storage} num, _ := number.Parse("ABC-123") - num.SetExternalFieldUncensor(true) + num.SetExternalFieldUnrated(true) fc := &model.FileContext{ Number: num, Meta: &model.MovieMeta{ @@ -268,7 +268,7 @@ func TestPosterCropHandlerUncensorCutterNoFaceRec(t *testing.T) { assert.NotNil(t, fc.Meta.Poster) } -func TestPosterCropHandlerUncensorCutterWithFaceRecError(t *testing.T) { +func TestPosterCropHandlerUnratedCutterWithFaceRecError(t *testing.T) { ctx := context.Background() storage := store.NewMemStorage() imgData := makeTestImage(t, 800, 600) @@ -277,7 +277,7 @@ func TestPosterCropHandlerUncensorCutterWithFaceRecError(t *testing.T) { faceRec := &mockFaceRec{err: errors.New("face rec error")} h := &posterCropHandler{storage: storage, faceRec: faceRec} num, _ := number.Parse("ABC-123") - num.SetExternalFieldUncensor(true) + num.SetExternalFieldUnrated(true) fc := &model.FileContext{ Number: num, Meta: &model.MovieMeta{ @@ -320,7 +320,7 @@ func TestPosterCropHandlerCensorCutterWithFaceRecCutError(t *testing.T) { assert.Error(t, err) } -func TestPosterCropHandlerUncensorCutterWithFaceRecSuccess(t *testing.T) { +func TestPosterCropHandlerUnratedCutterWithFaceRecSuccess(t *testing.T) { ctx := context.Background() storage := store.NewMemStorage() imgData := makeTestImage(t, 800, 600) @@ -331,7 +331,7 @@ func TestPosterCropHandlerUncensorCutterWithFaceRecSuccess(t *testing.T) { } h := &posterCropHandler{storage: storage, faceRec: faceRec} num, _ := number.Parse("ABC-123") - num.SetExternalFieldUncensor(true) + num.SetExternalFieldUnrated(true) fc := &model.FileContext{ Number: num, Meta: &model.MovieMeta{ diff --git a/internal/processor/handler/tag_padder_handler_test.go b/internal/processor/handler/tag_padder_handler_test.go index b79d7d3e..2c4d33d5 100644 --- a/internal/processor/handler/tag_padder_handler_test.go +++ b/internal/processor/handler/tag_padder_handler_test.go @@ -35,9 +35,9 @@ func TestTagPadderHandler(t *testing.T) { }, { name: "underscore separator", - numberID: "HEYZO_1234", + numberID: "PREFIX_1234", genres: nil, - wantPrefix: "HEYZO", + wantPrefix: "PREFIX", wantHas: true, }, } diff --git a/internal/processor/handler/watermark_handler.go b/internal/processor/handler/watermark_handler.go index 863eadb8..84b721ce 100644 --- a/internal/processor/handler/watermark_handler.go +++ b/internal/processor/handler/watermark_handler.go @@ -34,10 +34,10 @@ var defaultWatermarkRules = []watermarkRule{ {tag.Res4K, image.WM4K}, {tag.Res8K, image.WM8K}, {tag.VR, image.WMVR}, - {tag.Uncensored, image.WMUncensored}, + {tag.Unrated, image.WMUnrated}, {tag.ChineseSubtitle, image.WMChineseSubtitle}, - {tag.Leak, image.WMLeak}, - {tag.Hack, image.WMHack}, + {tag.SpecialEdition, image.WMSpecialEdition}, + {tag.Restored, image.WMRestored}, } // watermark handler 按 MovieMeta.Genres 驱动水印绘制, 不再读 Number diff --git a/internal/processor/handler/watermark_handler_test.go b/internal/processor/handler/watermark_handler_test.go index 949b1d9d..fd360204 100644 --- a/internal/processor/handler/watermark_handler_test.go +++ b/internal/processor/handler/watermark_handler_test.go @@ -54,7 +54,7 @@ func TestWatermarkHandlerStorageError(t *testing.T) { fc := &model.FileContext{ Meta: &model.MovieMeta{ Poster: &model.File{Name: "poster.jpg", Key: "nonexistent"}, - Genres: []string{tag.Uncensored}, + Genres: []string{tag.Unrated}, }, } assert.Error(t, h.Handle(context.Background(), fc)) @@ -89,9 +89,9 @@ func TestWatermarkHandlerAllTagTypes(t *testing.T) { {"8k", []string{tag.Res8K}}, {"VR", []string{tag.VR}}, {"chinese subtitle", []string{tag.ChineseSubtitle}}, - {"leak", []string{tag.Leak}}, - {"hack", []string{tag.Hack}}, - {"uncensor", []string{tag.Uncensored}}, + {"special_edition", []string{tag.SpecialEdition}}, + {"restored", []string{tag.Restored}}, + {"unrated", []string{tag.Unrated}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -115,14 +115,14 @@ func TestMatchWatermarksOrder(t *testing.T) { h := newWatermarkHandler(nil) // 所有 tag 都命中时, 顺序严格等于 defaultWatermarkRules. genres := []string{ - tag.Hack, tag.Leak, tag.ChineseSubtitle, - tag.Uncensored, tag.VR, tag.Res8K, tag.Res4K, + tag.Restored, tag.SpecialEdition, tag.ChineseSubtitle, + tag.Unrated, tag.VR, tag.Res8K, tag.Res4K, } got := h.matchWatermarks(genres) expect := []yimage.Watermark{ yimage.WM4K, yimage.WM8K, yimage.WMVR, - yimage.WMUncensored, yimage.WMChineseSubtitle, - yimage.WMLeak, yimage.WMHack, + yimage.WMUnrated, yimage.WMChineseSubtitle, + yimage.WMSpecialEdition, yimage.WMRestored, } assert.Equal(t, expect, got) } @@ -196,10 +196,10 @@ func TestMatchWatermarksCustomRules(t *testing.T) { h := &watermark{ rules: []watermarkRule{ {tag.VR, yimage.WMVR}, - {tag.Hack, yimage.WMHack}, + {tag.Restored, yimage.WMRestored}, }, } // 命中两条, 其它 tag 被忽略. - got := h.matchWatermarks([]string{tag.VR, tag.Hack, tag.Res4K}) - assert.Equal(t, []yimage.Watermark{yimage.WMVR, yimage.WMHack}, got) + got := h.matchWatermarks([]string{tag.VR, tag.Restored, tag.Res4K}) + assert.Equal(t, []yimage.Watermark{yimage.WMVR, yimage.WMRestored}, got) } diff --git a/internal/resource/image/hack.png b/internal/resource/image/restored.png similarity index 100% rename from internal/resource/image/hack.png rename to internal/resource/image/restored.png diff --git a/internal/resource/image/leak.png b/internal/resource/image/special_edition.png similarity index 100% rename from internal/resource/image/leak.png rename to internal/resource/image/special_edition.png diff --git a/internal/resource/image/uncensored.png b/internal/resource/image/unrated.png similarity index 100% rename from internal/resource/image/uncensored.png rename to internal/resource/image/unrated.png diff --git a/internal/resource/resource.go b/internal/resource/resource.go index d74857e0..4a98e185 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -7,14 +7,14 @@ import ( //go:embed image/subtitle.png var ResIMGSubtitle []byte -//go:embed image/uncensored.png -var ResIMGUncensored []byte +//go:embed image/unrated.png +var ResIMGUnrated []byte //go:embed image/4k.png var ResIMG4K []byte -//go:embed image/leak.png -var ResIMGLeak []byte +//go:embed image/special_edition.png +var ResIMGSpecialEdition []byte //go:embed image/8k.png var ResIMG8K []byte @@ -22,8 +22,8 @@ var ResIMG8K []byte //go:embed image/vr.png var ResIMGVR []byte -//go:embed image/hack.png -var ResIMGHack []byte +//go:embed image/restored.png +var ResIMGRestored []byte //go:embed json/c_number.json.gz var ResCNumber []byte // 数据来源为mdcx diff --git a/internal/scanner/scanner_test.go b/internal/scanner/scanner_test.go index b4d02a3d..a233abe5 100644 --- a/internal/scanner/scanner_test.go +++ b/internal/scanner/scanner_test.go @@ -166,7 +166,7 @@ func TestScanRejectsReentryWhileRunning(t *testing.T) { require.NoError(t, sqlite.Close()) }) - filePath := filepath.Join(scanDir, "HEYZO-0040.mp4") + filePath := filepath.Join(scanDir, "DEMO-0040.mp4") require.NoError(t, os.WriteFile(filePath, []byte("x"), 0o600)) cleaner := &blockingCleaner{ diff --git a/internal/searcher/debugger.go b/internal/searcher/debugger.go index 6c8d29ad..7a81c91b 100644 --- a/internal/searcher/debugger.go +++ b/internal/searcher/debugger.go @@ -41,7 +41,7 @@ type DebugSearchResult struct { MatchedPlugin string `json:"matched_plugin"` Found bool `json:"found"` Category string `json:"category"` - Uncensor bool `json:"uncensor"` + Unrated bool `json:"unrated"` CleanerResult *movieidcleaner.Result `json:"cleaner_result,omitempty"` Meta *model.MovieMeta `json:"meta,omitempty"` PluginResults []PluginDebugResult `json:"plugin_results"` @@ -214,9 +214,9 @@ func (d *Debugger) tryCleanInput(input string, result *DebugSearchResult) (*numb num.SetExternalFieldCategory(cleanRes.Category) result.Category = cleanRes.Category } - if cleanRes.UncensorMatched { - num.SetExternalFieldUncensor(cleanRes.Uncensor) - result.Uncensor = cleanRes.Uncensor + if cleanRes.UnratedMatched { + num.SetExternalFieldUnrated(cleanRes.Unrated) + result.Unrated = cleanRes.Unrated } return num, nil } diff --git a/internal/searcher/debugger_test.go b/internal/searcher/debugger_test.go index eaaeac4a..5be5dc36 100644 --- a/internal/searcher/debugger_test.go +++ b/internal/searcher/debugger_test.go @@ -155,14 +155,14 @@ func TestDebugSearch_CleanerNilResult(t *testing.T) { require.False(t, result.Found) } -func TestDebugSearch_WithUncensor(t *testing.T) { +func TestDebugSearch_WithUnrated(t *testing.T) { ctx := factory.NewRegisterContext() ctx.Register("test-plg", func(_ any) (api.IPlugin, error) { return &precheckFalsePlugin{}, nil }) cleaner := &mockCleaner{ res: &movieidcleaner.Result{ - Normalized: "ABC-123", - UncensorMatched: true, - Uncensor: true, + Normalized: "ABC-123", + UnratedMatched: true, + Unrated: true, }, } d := &Debugger{ @@ -176,7 +176,7 @@ func TestDebugSearch_WithUncensor(t *testing.T) { UseCleaner: true, }) require.NoError(t, err) - require.True(t, result.Uncensor) + require.True(t, result.Unrated) } func TestDebugSearch_ResolvePluginsWithCategory(t *testing.T) { diff --git a/internal/searcher/plugin/yaml/plugin_test.go b/internal/searcher/plugin/yaml/plugin_test.go index 30bb1d27..dc1b9002 100644 --- a/internal/searcher/plugin/yaml/plugin_test.go +++ b/internal/searcher/plugin/yaml/plugin_test.go @@ -89,7 +89,7 @@ scrape: require.False(t, ok) } -func TestYAML_Jav321_OneStep(t *testing.T) { +func TestYAML_OneStep_PostForm(t *testing.T) { var baseURL string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/img/") { @@ -119,13 +119,13 @@ func TestYAML_Jav321_OneStep(t *testing.T) { - 品番: ABC-123 - 出演者AliceBob - 配信開始日: 2024-01-02 - 収録時間: 120 分钟 - メーカーStudio A - シリーズ: Series X - ジャンルDramaTag2 + Number: ABC-123 + CastAliceBob + Release Date: 2024-01-02 + Runtime: 120 分钟 + StudioStudio A + Series: Series X + GenreDramaTag2

`, "{{HOST}}", baseURL))) @@ -134,7 +134,7 @@ func TestYAML_Jav321_OneStep(t *testing.T) { baseURL = srv.URL plg := mustPluginFromYAML(t, strings.ReplaceAll(oneStepFixtureYAML(), "https://fixture.example", srv.URL)) - meta := mustSearch(t, "jav321", plg, srv.Client(), "ABC-123") + meta := mustSearch(t, "demo-onestep", plg, srv.Client(), "ABC-123") require.Equal(t, "ABC-123", meta.Number) require.Equal(t, "Sample Title", meta.Title) require.Equal(t, []string{"Alice", "Bob"}, meta.Actors) @@ -150,7 +150,7 @@ func TestYAML_Jav321_OneStep(t *testing.T) { require.EqualValues(t, 120*60, meta.Duration) } -func TestYAML_JavDB_TwoStep(t *testing.T) { +func TestYAML_TwoStep_HTMLList(t *testing.T) { var baseURL string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/img/") { @@ -180,14 +180,14 @@ func TestYAML_JavDB_TwoStep(t *testing.T) { _, _ = w.Write([]byte(strings.ReplaceAll(` -

JavDB Title

-
演員AliceBob
-
日期2024-05-06
-
時長150 分钟
-
片商Studio J
-
系列Series J
-
類別DramaAction
-
+

Sample Twostep Title

+
CastAliceBob
+
Release Date2024-05-06
+
Runtime150 分钟
+
StudioStudio J
+
SeriesSeries J
+
GenreDramaAction
+
`, "{{HOST}}", baseURL))) default: @@ -198,21 +198,21 @@ func TestYAML_JavDB_TwoStep(t *testing.T) { baseURL = srv.URL plg := mustPluginFromYAML(t, strings.ReplaceAll(twoStepFixtureYAML(), "https://fixture.example", srv.URL)) - meta := mustSearch(t, "javdb", plg, srv.Client(), "ABC-123") + meta := mustSearch(t, "demo-twostep", plg, srv.Client(), "ABC-123") require.Equal(t, "ABC-123", meta.Number) - require.Equal(t, "JavDB Title", meta.Title) + require.Equal(t, "Sample Twostep Title", meta.Title) require.Equal(t, []string{"Alice", "Bob"}, meta.Actors) require.Equal(t, "Studio J", meta.Studio) require.Equal(t, "Series J", meta.Series) require.Equal(t, []string{"Drama", "Action"}, meta.Genres) - require.Equal(t, srv.URL+"/img/javdb-cover.jpg", meta.Cover.Name) + require.Equal(t, srv.URL+"/img/sample-cover.jpg", meta.Cover.Name) require.Len(t, meta.SampleImages, 2) require.Equal(t, enum.MetaLangJa, meta.TitleLang) require.NotZero(t, meta.ReleaseDate) require.EqualValues(t, 150*60, meta.Duration) } -func TestYAML_Airav_JSON(t *testing.T) { +func TestYAML_JSON_OneStep(t *testing.T) { var baseURL string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/img/") { @@ -221,7 +221,7 @@ func TestYAML_Airav_JSON(t *testing.T) { return } require.Equal(t, http.MethodGet, r.Method) - require.Equal(t, "/api/video/barcode/ABC-123", r.URL.Path) + require.Equal(t, "/api/video/code/ABC-123", r.URL.Path) require.Equal(t, "zh-TW", r.URL.Query().Get("lng")) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(strings.ReplaceAll(`{ @@ -229,8 +229,8 @@ func TestYAML_Airav_JSON(t *testing.T) { "status": "ok", "result": { "barcode": "ABC-123", - "name": "Airav Title", - "description": "Airav Plot", + "name": "Sample JSON Title", + "description": "Sample JSON Plot", "img_url": "{{HOST}}/img/cover.jpg", "publish_date": "2024-05-06", "actors": [{"name": "Alice"}, {"name": "Bob"}], @@ -244,10 +244,10 @@ func TestYAML_Airav_JSON(t *testing.T) { baseURL = srv.URL plg := mustPluginFromYAML(t, strings.ReplaceAll(jsonFixtureYAML(), "https://fixture.example", srv.URL)) - meta := mustSearch(t, "airav", plg, srv.Client(), "ABC-123") + meta := mustSearch(t, "demo-jsonapi", plg, srv.Client(), "ABC-123") require.Equal(t, "ABC-123", meta.Number) - require.Equal(t, "Airav Title", meta.Title) - require.Equal(t, "Airav Plot", meta.Plot) + require.Equal(t, "Sample JSON Title", meta.Title) + require.Equal(t, "Sample JSON Plot", meta.Plot) require.Equal(t, []string{"Alice", "Bob"}, meta.Actors) require.Equal(t, "Studio A", meta.Studio) require.Equal(t, []string{"Drama", "Action"}, meta.Genres) @@ -298,7 +298,7 @@ scrape: number: selector: kind: xpath - expr: //b[contains(text(),"品番")]/following-sibling::node()[1] + expr: //b[contains(text(),"Number")]/following-sibling::node()[1] transforms: - kind: trim_charset cutset: ": \t" @@ -324,7 +324,7 @@ scrape: release_date: selector: kind: xpath - expr: //b[contains(text(),"配信開始日")]/following-sibling::node()[1] + expr: //b[contains(text(),"Release Date")]/following-sibling::node()[1] transforms: - kind: trim_charset cutset: ": \t" @@ -334,7 +334,7 @@ scrape: duration: selector: kind: xpath - expr: //b[contains(text(),"収録時間")]/following-sibling::node()[1] + expr: //b[contains(text(),"Runtime")]/following-sibling::node()[1] transforms: - kind: trim_charset cutset: ": \t" @@ -342,17 +342,17 @@ scrape: studio: selector: kind: xpath - expr: //b[contains(text(),"メーカー")]/following-sibling::a[1]/text() + expr: //b[contains(text(),"Studio")]/following-sibling::a[1]/text() parser: string label: selector: kind: xpath - expr: //b[contains(text(),"メーカー")]/following-sibling::a[1]/text() + expr: //b[contains(text(),"Studio")]/following-sibling::a[1]/text() parser: string series: selector: kind: xpath - expr: //b[contains(text(),"シリーズ")]/following-sibling::node()[1] + expr: //b[contains(text(),"Series")]/following-sibling::node()[1] transforms: - kind: trim_charset cutset: ": \t" @@ -360,7 +360,7 @@ scrape: genres: selector: kind: xpath - expr: //b[contains(text(),"ジャンル")]/following-sibling::a/text() + expr: //b[contains(text(),"Genre")]/following-sibling::a/text() multi: true parser: string_list cover: @@ -429,35 +429,35 @@ scrape: actors: selector: kind: xpath - expr: //strong[contains(text(),"演員")]/following-sibling::span[@class="value"]/a/text() + expr: //strong[contains(text(),"Cast")]/following-sibling::span[@class="value"]/a/text() multi: true parser: string_list release_date: selector: kind: xpath - expr: //strong[contains(text(),"日期")]/following-sibling::span[@class="value"]/text() + expr: //strong[contains(text(),"Release Date")]/following-sibling::span[@class="value"]/text() parser: kind: time_format layout: "2006-01-02" duration: selector: kind: xpath - expr: //strong[contains(text(),"時長")]/following-sibling::span[@class="value"]/text() + expr: //strong[contains(text(),"Runtime")]/following-sibling::span[@class="value"]/text() parser: duration_default studio: selector: kind: xpath - expr: //strong[contains(text(),"片商")]/following-sibling::span[@class="value"]/text() + expr: //strong[contains(text(),"Studio")]/following-sibling::span[@class="value"]/text() parser: string series: selector: kind: xpath - expr: //strong[contains(text(),"系列")]/following-sibling::span[@class="value"]/text() + expr: //strong[contains(text(),"Series")]/following-sibling::span[@class="value"]/text() parser: string genres: selector: kind: xpath - expr: //strong[contains(text(),"類別")]/following-sibling::span[@class="value"]/a/text() + expr: //strong[contains(text(),"Genre")]/following-sibling::span[@class="value"]/a/text() multi: true parser: string_list cover: @@ -486,7 +486,7 @@ hosts: - https://fixture.example request: method: GET - path: /api/video/barcode/${number} + path: /api/video/code/${number} query: lng: zh-TW scrape: diff --git a/internal/tag/constants.go b/internal/tag/constants.go index 0a827b56..82855123 100644 --- a/internal/tag/constants.go +++ b/internal/tag/constants.go @@ -10,12 +10,16 @@ // 不要往这里塞。 package tag +// 常量名刻意使用电影发行行业通用术语 (Unrated / SpecialEdition / +// Restored), 与 MPAA / 院线发行常见分类对齐。展示值保持既有中文 +// 字符串以保证向后兼容: 已入库的 MovieMeta.Genres、用户 tag_mapper +// 配置、watermark 规则都能继续命中。 const ( - Uncensored = "未审查" + Unrated = "未审查" ChineseSubtitle = "字幕版" Res4K = "4K" Res8K = "8K" VR = "VR" - Leak = "特别版" - Hack = "修复版" + SpecialEdition = "特别版" + Restored = "修复版" ) diff --git a/web/src/components/handler-debug-shell/__tests__/use-handler-debug-state.test.tsx b/web/src/components/handler-debug-shell/__tests__/use-handler-debug-state.test.tsx index 6e71a931..bb034e02 100644 --- a/web/src/components/handler-debug-shell/__tests__/use-handler-debug-state.test.tsx +++ b/web/src/components/handler-debug-shell/__tests__/use-handler-debug-state.test.tsx @@ -39,7 +39,7 @@ function makeResult(overrides: Partial = {}): HandlerDebugRe handler_name: "", number_id: "ABC-123", category: "", - uncensor: false, + unrated: false, before_meta: { number: "ABC-123" } as HandlerDebugResult["before_meta"], after_meta: { number: "ABC-123", title: "new" } as HandlerDebugResult["after_meta"], error: "", diff --git a/web/src/components/library-shell/__tests__/utils.test.ts b/web/src/components/library-shell/__tests__/utils.test.ts index 18fac0b2..648befb1 100644 --- a/web/src/components/library-shell/__tests__/utils.test.ts +++ b/web/src/components/library-shell/__tests__/utils.test.ts @@ -180,8 +180,8 @@ describe("cloneMeta", () => { }); it("preserves all scalar fields", () => { - const src = baseMeta({ title: "T", runtime: 42, source: "javdb" }); - expect(cloneMeta(src)).toMatchObject({ title: "T", runtime: 42, source: "javdb" }); + const src = baseMeta({ title: "T", runtime: 42, source: "demo" }); + expect(cloneMeta(src)).toMatchObject({ title: "T", runtime: 42, source: "demo" }); }); }); diff --git a/web/src/components/ruleset-debug-shell.tsx b/web/src/components/ruleset-debug-shell.tsx index 64722fcd..a58fdfdd 100644 --- a/web/src/components/ruleset-debug-shell.tsx +++ b/web/src/components/ruleset-debug-shell.tsx @@ -126,7 +126,7 @@ export function RulesetDebugShell() {
附加标记 - {result.final.uncensor ? "true" : "false"} + {result.final.unrated ? "true" : "false"}
命中规则 diff --git a/web/src/components/searcher-debug-shell.tsx b/web/src/components/searcher-debug-shell.tsx index 5308d366..9fed46c0 100644 --- a/web/src/components/searcher-debug-shell.tsx +++ b/web/src/components/searcher-debug-shell.tsx @@ -226,7 +226,7 @@ export function SearcherDebugShell() {
分类 / 附加标记 - {result.category || "-"} / {result.uncensor ? "true" : "false"} + {result.category || "-"} / {result.unrated ? "true" : "false"}
diff --git a/web/src/lib/__tests__/api-coverage.test.ts b/web/src/lib/__tests__/api-coverage.test.ts index dc0a246c..73595db5 100644 --- a/web/src/lib/__tests__/api-coverage.test.ts +++ b/web/src/lib/__tests__/api-coverage.test.ts @@ -637,7 +637,7 @@ describe("debug API", () => { matched_plugin: "", found: false, category: "", - uncensor: false, + unrated: false, plugin_results: [], available_tools: {} as never, }; @@ -660,7 +660,7 @@ describe("debug API", () => { matched_plugin: "", found: false, category: "", - uncensor: false, + unrated: false, plugin_results: [], available_tools: {} as never, }; @@ -690,7 +690,7 @@ describe("debug API", () => { handler_name: "Trim", number_id: "", category: "", - uncensor: false, + unrated: false, before_meta: {}, after_meta: {}, error: "", diff --git a/web/src/lib/api/debug.ts b/web/src/lib/api/debug.ts index 6afbf31c..2b53954f 100644 --- a/web/src/lib/api/debug.ts +++ b/web/src/lib/api/debug.ts @@ -21,8 +21,8 @@ export interface MovieIDCleanerCandidate { end: number; category: string; category_matched: boolean; - uncensor: boolean; - uncensor_matched: boolean; + unrated: boolean; + unrated_matched: boolean; } export interface MovieIDCleanerExplainStep { @@ -44,9 +44,9 @@ export interface MovieIDCleanerResult { number_id: string; suffixes: string[] | null; category: string; - uncensor: boolean; + unrated: boolean; category_matched: boolean; - uncensor_matched: boolean; + unrated_matched: boolean; confidence: string; status: string; rule_hits: string[] | null; @@ -136,7 +136,7 @@ export interface SearcherDebugResult { matched_plugin: string; found: boolean; category: string; - uncensor: boolean; + unrated: boolean; cleaner_result?: MovieIDCleanerResult | null; meta?: SearcherDebugMovieMeta | null; plugin_results: SearcherDebugPluginResult[] | null; @@ -183,7 +183,7 @@ export interface HandlerDebugResult { handler_name: string; number_id: string; category: string; - uncensor: boolean; + unrated: boolean; before_meta: SearcherDebugMovieMeta; after_meta: SearcherDebugMovieMeta; error: string;