import { describe, expect, it } from "vitest"; import { type IdentityCandidate, normalizeTitle, parseRelease, queryTitles, rankCandidates, resolveIdentity, scoreCandidate, stripSeasonSuffix, TRAD_TO_SIMP, toSimplified, } from "../src/index.ts"; const movie = (over: Partial = {}): IdentityCandidate => ({ provider: "tmdb", entityType: "movie", externalId: "872585", titles: ["Oppenheimer", "奥本海默"], year: 2023, aliases: [], ...over, }); describe("标题归一化", () => { it("繁简 / 全半角 / 标点 / 大小写", () => { expect(toSimplified("葬送的芙莉蓮")).toBe("葬送的芙莉莲"); expect(normalizeTitle("葬送的芙莉蓮")).toBe(normalizeTitle("葬送的芙莉莲")); expect(normalizeTitle("THE LAST OF US")).toBe("thelastofus"); expect(normalizeTitle("Spider-Man: No Way Home")).toBe("spidermannowayhome"); expect(normalizeTitle("Tom & Jerry")).toBe("tomandjerry"); expect(normalizeTitle("黑暗榮耀")).toBe("黑暗荣耀"); }); it("去季号/年份后缀", () => { expect(stripSeasonSuffix("庆余年 第二季")).toBe("庆余年"); expect(stripSeasonSuffix("The Boys Season 4")).toBe("The Boys"); expect(stripSeasonSuffix("Show S02")).toBe("Show"); expect(stripSeasonSuffix("Title (2019)")).toBe("Title"); expect(stripSeasonSuffix("第二季")).toBe("第二季"); expect(normalizeTitle("庆余年 第二季")).toBe(normalizeTitle("慶餘年")); }); it("映射表是一一对应的字符对", () => { expect(TRAD_TO_SIMP.size).toBeGreaterThan(500); for (const [k, v] of TRAD_TO_SIMP) { expect([...k].length).toBe(1); expect([...v].length).toBe(1); } }); it("queryTitles 去重", () => { const p = parseRelease("奥本海默 Oppenheimer 2023 1080p"); expect(queryTitles(p)).toEqual(["奥本海默", "oppenheimer"]); }); }); describe("候选打分(证据等级,无概率)", () => { const p = parseRelease("Oppenheimer.2023.2160p.UHD.BluRay.REMUX-FraMeSToR"); it("标题精确 + 年份一致 → rule_confirmed", () => { const r = scoreCandidate(p, movie()); expect(r.level).toBe("rule_confirmed"); expect(r.titleMatch).toBe("exact"); expect(r.yearMatch).toBe("exact"); expect(Object.values(r).some((v) => typeof v === "number")).toBe(false); }); it("年份容差 ±1 → multi_evidence;±2 → rejected", () => { expect(scoreCandidate(p, movie({ year: 2024 })).level).toBe("multi_evidence"); expect(scoreCandidate(p, movie({ year: 2022 })).yearMatch).toBe("within_tolerance"); expect(scoreCandidate(p, movie({ year: 2025 })).level).toBe("rejected"); expect(scoreCandidate(p, movie({ year: 2025 }), { yearTolerance: 2 }).level).toBe("multi_evidence"); }); it("别名命中 → multi_evidence;部分包含 → needs_confirm;无关 → rejected", () => { expect(scoreCandidate(p, movie({ titles: ["X"], aliases: ["Oppenheimer"] })).level).toBe("multi_evidence"); expect(scoreCandidate(p, movie({ titles: ["Oppenheimer Returns"] })).level).toBe("needs_confirm"); expect(scoreCandidate(p, movie({ titles: ["Barbie"] })).level).toBe("rejected"); }); it("年份未知:电影需确认,剧集有季号且类型一致算多证据", () => { const noYear = parseRelease("Oppenheimer 1080p WEB-DL"); expect(scoreCandidate(noYear, movie()).level).toBe("needs_confirm"); const tv = parseRelease("奔跑吧 第十二季 20240426 第1期 1080p WEB-DL"); const show: IdentityCandidate = { provider: "tmdb", entityType: "tv", externalId: "1", titles: ["奔跑吧"], year: 2017, aliases: [], }; const r = scoreCandidate(tv, show); expect(r.yearMatch).toBe("unknown"); expect(r.typeMatch).toBe("match"); expect(r.level).toBe("multi_evidence"); expect(scoreCandidate(tv, { ...show, entityType: "movie" }).level).toBe("needs_confirm"); }); it("剧集季年份:seasonYears 命中算 season_year;后续季无季年份信息不据此拒绝", () => { const s2 = parseRelease("The.Last.of.Us.S02.2025.1080p.WEB-DL"); const show: IdentityCandidate = { provider: "tmdb", entityType: "tv", externalId: "100088", titles: ["The Last of Us"], year: 2023, aliases: [], }; expect(scoreCandidate(s2, show).yearMatch).toBe("unknown"); const withYears = scoreCandidate(s2, { ...show, seasonYears: [2023, 2025] }); expect(withYears.yearMatch).toBe("season_year"); expect(withYears.level).toBe("rule_confirmed"); expect(scoreCandidate(s2, { ...show, seasonYears: [2023] }).level).toBe("rejected"); }); it("显式 ID 提升一级;标题不符但显式 ID 命中 → needs_confirm", () => { const e = parseRelease("[tmdbid=872585] Oppenheimer 2024 1080p"); const r = scoreCandidate(e, movie()); expect(r.evidence).toContain("explicit_id"); expect(r.level).toBe("rule_confirmed"); const e2 = parseRelease("[tmdbid=872585] Something Else 2023 1080p"); expect(scoreCandidate(e2, movie()).level).toBe("needs_confirm"); const local = movie({ provider: "local", externalId: "uuid-1", linkedExternalIds: [{ provider: "tmdb", id: "872585" }], }); expect(scoreCandidate(e2, local).evidence).toContain("explicit_id"); }); }); describe("排序与判定", () => { const p = parseRelease("Oppenheimer.2023.1080p.BluRay.x264-CHD"); it("本地实体同等级优先,等级 > 标题 > 年份", () => { const tmdb = movie(); const local = movie({ provider: "local", externalId: "uuid-1", localEntityId: "uuid-1" }); const weak = movie({ externalId: "9", titles: ["Oppenheimer X"] }); const ranked = rankCandidates(p, [weak, tmdb, local]); expect(ranked.map((r) => r.candidate.externalId)).toEqual(["uuid-1", "872585", "9"]); }); it("唯一强候选 → auto", () => { const res = resolveIdentity(p, [movie(), movie({ externalId: "2", titles: ["Barbie"] })]); expect(res.status).toBe("auto"); expect(res.best?.candidate.externalId).toBe("872585"); }); it("同等级两个不同身份 → ambiguous;同一身份(本地映射)不算冲突", () => { const dup = resolveIdentity(p, [movie(), movie({ externalId: "999" })]); expect(dup.status).toBe("ambiguous"); expect(dup.reason).toBe("multiple_candidates_same_level"); const local = movie({ provider: "local", externalId: "uuid-1", localEntityId: "uuid-1", linkedExternalIds: [{ provider: "tmdb", id: "872585" }], }); expect(resolveIdentity(p, [movie(), local]).status).toBe("auto"); expect(resolveIdentity(p, [local, movie()]).status).toBe("auto"); expect(resolveIdentity(p, [local, { ...local, externalId: "x" }]).status).toBe("auto"); }); it("弱证据 → ambiguous;无候选 → no_match", () => { expect(resolveIdentity(p, [movie({ titles: ["Oppenheimer Returns"] })]).status).toBe("ambiguous"); expect(resolveIdentity(p, []).status).toBe("no_match"); expect(resolveIdentity(p, [movie({ titles: ["Barbie"] })]).status).toBe("no_match"); }); });