import { calculateEpisodeOffset } from "@umt/contracts"; import { describe, expect, it } from "vitest"; import { applyGlobalWords, compileGlobalWords, parseRelease, parseReleasePath, smartNumber, toChineseNumber, } from "../src/index.ts"; // 全局识别词套用(移植 MP WordsMatcher)。MP 用例出处:tests/test_metainfo.py、tests/test_rust_accel.py。 const words = (...lines: string[]) => compileGlobalWords(lines.join("\n")); const parse = (title: string, w: string[], subtitle?: string) => parseRelease(title, { words: words(...w), ...(subtitle ? { subtitle } : {}) }); describe("MP 识别词用例", () => { it("复杂识别词先替换后偏移(test_custom_words_replace_then_episode_offset)", () => { const w = "旧名 => 新名 && 第 <> 集 >> EP+1"; const r = parse("旧名 第03集", [w]); expect(r.title).toBe("新名"); expect(r.episodes).toEqual({ start: 4, end: 4 }); expect(r.words?.applied).toEqual([w]); expect(r.words?.original).toBe("旧名 第03集"); }); it("标题没有集数时只偏移副标题里的集数,季数不动(test_custom_words_episode_offset_applies_to_subtitle)", () => { const w = "BLEACH Thousand-Year Blood War S04 => BLEACH 2004 S02 && S02 <> 1080p >> EP+40"; const sub = "死神 千年血战篇 -祸进谭- 第四季 第8集 ..."; const out = applyGlobalWords( "BLEACH Thousand-Year Blood War S04 1080p Disney+ WEB-DL AAC 2.0 H.264-CHDWEB", sub, words(w), ); expect(out.subtitle).toBe("死神 千年血战篇 -祸进谭- 第四季 第48集 ..."); expect(out.applied).toEqual([w]); const r = parse("BLEACH Thousand-Year Blood War S04 1080p Disney+ WEB-DL AAC 2.0 H.264-CHDWEB", [w], sub); expect(r.season).toBe(2); expect(r.episodes).toEqual({ start: 48, end: 48 }); }); it("替换里带 {[tmdbid]} 并跨季偏移(test_get_torrent_episodes_applies_custom_words)", () => { const w = "A.Will.Eternal.S04 => 一念永恒{[tmdbid=107371;type=tv]}S01 && S01 <> 2160p >> EP+165"; const r = parse("A.Will.Eternal.S04E05.2026.2160p.WEB-DL.mkv", [w]); expect(r.episodes).toEqual({ start: 170, end: 170 }); expect(r.season).toBe(1); expect(r.explicitIds).toContainEqual({ provider: "tmdb", id: "107371" }); }); it("偏移表达式支持乘法与分组引用 \\1(test_custom_words_episode_offset_supports_multiplication_expression)", () => { const w = "Ha.Ha.Ha.Ha.Ha.2026.S06E([0-1][0-9]).Part1 => 哈哈哈哈哈 (2020){[tmdbid=112732;type=tv]} S06E\\1.Part1 && S06 <> .Part1 >> 2*EP-1"; const r = parse("Ha.Ha.Ha.Ha.Ha.2026.S06E03.Part1", [w]); expect(r.title).toBe("哈哈哈哈哈"); expect(r.explicitIds).toContainEqual({ provider: "tmdb", id: "112732" }); expect(r.season).toBe(6); expect(r.episodes).toEqual({ start: 5, end: 5 }); expect(r.words?.applied).toEqual([w]); }); it("EP 可以出现多次(test_custom_words_episode_offset_supports_repeated_ep_expression)", () => { const r = parse("旧名 第03集", ["旧名 => 新名 && 第 <> 集 >> EP+EP-1"]); expect(r.title).toBe("新名"); expect(r.episodes).toEqual({ start: 5, end: 5 }); }); it("2EP 不当成乘法:替换照做、偏移不做、这一行不算命中(test_custom_words_episode_offset_rejects_implicit_ep_expression)", () => { const r = parse("旧名 第03集", ["旧名 => 新名 && 第 <> 集 >> 2EP"]); expect(r.title).toBe("新名"); expect(r.episodes).toEqual({ start: 3, end: 3 }); expect(r.words).toBeUndefined(); }); it("{[…;g=…;s=1]} 指定作品与季(test_custom_words_support_episode_group_parameter)", () => { const w = "Bakemonogatari => 物语系列 {[tmdbid=46195;type=tv;g=5ad0ec240e0a26303f00d84d;s=1]}"; const r = parse("Bakemonogatari 01", [w]); expect(r.explicitIds).toContainEqual({ provider: "tmdb", id: "46195" }); expect(r.season).toBe(1); expect(r.mediaTypeHint).toBe("tv"); expect(r.words?.applied).toEqual([w]); }); it("s=0 指定为特别季(test_custom_words_support_special_season_zero_parameter)", () => { const r = parse("Test Show 01", ["Test Show => 测试剧 {[tmdbid=12345;type=tv;s=0]}"]); expect(r.explicitIds).toContainEqual({ provider: "tmdb", id: "12345" }); expect(r.season).toBe(0); }); it("替换为空串删掉词(test_metainfo_preserves_original_name_when_custom_words_applied)", () => { const r = parse("电影测试替换名称 (2024)", ["测试替换 => "]); expect(r.title).toBe("电影名称"); expect(r.year).toBe(2024); }); it("记录命中的识别词(test_custom_words_apply_words_recording)", () => { const r = parse("电影替换词.2024.mkv", ["替换词 => 新词"]); expect(r.words?.applied).toEqual(["替换词 => 新词"]); }); it("路径识别同样套用识别词(test_metainfopath_with_custom_words)", () => { const r = parseReleasePath("/movies/电影测试替换名称 (2024)/电影测试替换名称.mkv", { words: words("测试替换 => "), }); expect(r.title).toBe("电影名称"); expect(r.year).toBe(2024); expect(r.words?.applied).toEqual(["测试替换 => "]); }); }); describe("识别词行为细节", () => { it("屏蔽词:整行当正则删掉", () => { expect(applyGlobalWords("Show.S01E01.国语中字.1080p", null, words("国语中字")).title).toBe("Show.S01E01..1080p"); }); it("注释、空行跳过;正则写错的行跳过,不影响后面的行", () => { const out = applyGlobalWords("A B C", null, words("# A => X", "", "([ => Y", "B => Z")); expect(out.title).toBe("A Z C"); expect(out.applied).toEqual(["B => Z"]); }); it("按行序处理:前一行的结果交给后一行", () => { expect(applyGlobalWords("abc", null, words("a => b", "bb => X")).title).toBe("Xc"); }); it("Python 写法:命名分组 (?P…)、\\g、(?i)", () => { const out = applyGlobalWords("Foo.S01E02", null, words("(?i)foo\\.S(?P\\d+) => Bar.S\\g")); expect(out.title).toBe("Bar.S01E02"); }); it("只偏移前后定位词之间的数字;前导零保留宽度;向前偏移", () => { expect(applyGlobalWords("[Grp] Show - 25 [1080p]", null, words("Show - <> \\[1080p >> EP-12")).title).toBe( "[Grp] Show - 13 [1080p]", ); expect(applyGlobalWords("Show E05 end", null, words("Show E <> end >> EP+10")).title).toBe("Show E15 end"); }); it("中文集数偏移后仍写中文", () => { expect(applyGlobalWords("第十二集", null, words("第 <> 集 >> EP+1")).title).toBe("第十三集"); }); it("定位词不在标题里就不动", () => { const out = applyGlobalWords("Other 05", null, words("Show <> end >> EP+1")); expect(out).toEqual({ title: "Other 05", subtitle: null, applied: [] }); }); it("中文数字互转", () => { expect(smartNumber("十二")).toBe(12); expect(smartNumber("一二")).toBe(12); expect(smartNumber("08")).toBe(8); expect(toChineseNumber(13)).toBe("十三"); expect(toChineseNumber(105)).toBe("一百零五"); expect(toChineseNumber(-3)).toBe("负三"); }); }); describe("集号算术(MP calculate_episode_offset)", () => { it("白名单运算", () => { expect(calculateEpisodeOffset("EP+1", 3)).toBe(4); expect(calculateEpisodeOffset("2*EP-1", 3)).toBe(5); expect(calculateEpisodeOffset("EP+EP-1", 3)).toBe(5); expect(calculateEpisodeOffset("(EP+1)//2", 5)).toBe(3); expect(calculateEpisodeOffset("EP/2", 5)).toBe(2); expect(calculateEpisodeOffset("-EP+20", 5)).toBe(15); expect(calculateEpisodeOffset("EP%4", 10)).toBe(2); expect(calculateEpisodeOffset("-7%3", 0)).toBe(2); expect(calculateEpisodeOffset("5", 3)).toBe(5); }); it("拒绝省略运算符、乘方、名字与非法字符", () => { for (const bad of ["2EP", "EP(1)", "EP**2", "EPX+1", "ep+1", "EP+1;", "EP/0", "EP+"]) { expect(() => calculateEpisodeOffset(bad, 1), bad).toThrow(); } }); });