- server 的 src/modules/*/__tests__ 挪到 test/<模块>/;e2e 去掉 eN- 前缀, w5a2 / v2 / search2 / *-dh1 / *-md1 / *-md2、web 的 agui3 / search2、domain 的 id2 / id3 改为按行为命名 - vitest 分三组:unit、pg(真实 PG + 假引擎,文件并行,最多 6 个)、serial(真实引擎 / worker / e2e,串行); pg 组用例超时放宽到 60s(迁移类用例并行时排队等 schema 锁)。server 测试 615s → 约 350s - CLAUDE.md 补测试约定;待办补 harness 去重与 continuous-series 偶发超时(疑似 Slot 评估竞争) - ADR-004 补充:维持 Deno,记录实际只用到 --allow-run 白名单 Co-Authored-By: Claude Opus 5.5 <[email protected]>
174 lines
8.1 KiB
TypeScript
174 lines
8.1 KiB
TypeScript
/** ID-2:副标题 / RSS 内嵌描述片名、分类前缀、续作序号、通用词守卫、评分权重与同级取舍 */
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
DEFAULT_MATCH_WEIGHTS,
|
|
type IdentityCandidate,
|
|
isGenericAliasName,
|
|
normalizeTitle,
|
|
parseRelease,
|
|
queryTitles,
|
|
resolveIdentity,
|
|
scoreCandidate,
|
|
sequelNumbers,
|
|
} from "../src/index.ts";
|
|
|
|
const tv = (id: string, titles: string[], year: number, over: Partial<IdentityCandidate> = {}): IdentityCandidate => ({
|
|
provider: "tmdb",
|
|
entityType: "tv",
|
|
externalId: id,
|
|
titles,
|
|
year,
|
|
aliases: [],
|
|
...over,
|
|
});
|
|
|
|
describe("解析:分类前缀、内嵌描述、副标题片名", () => {
|
|
it("RSS `[动漫]英文名 … -GRP[中文名 / 别名 | 第N集 | …][体积]`", () => {
|
|
const p = parseRelease(
|
|
"[动漫]Martial Master S01 2020 1080p WEB-DL H.264 AAC2.0-HHWEB[武神主宰 / 武神主宰 第一季 / The God of War Dominates | 第685集 | 1080p | 类型: 动画/奇幻/冒险 | 导演: 老鹰][180.86 MB]",
|
|
);
|
|
expect(p).toMatchObject({
|
|
category: "动漫",
|
|
titleCn: "武神主宰",
|
|
titleEn: "Martial Master",
|
|
year: 2020,
|
|
season: 1,
|
|
episodes: { start: 685, end: 685 },
|
|
altTitles: ["武神主宰 第一季", "The God of War Dominates"],
|
|
});
|
|
expect(p.evidence).toContain("subtitle:embedded");
|
|
});
|
|
it("分类词不当片名:[电视剧] / [音乐] / [综艺]", () => {
|
|
for (const c of ["电视剧", "音乐", "综艺", "纪录片", "短剧", "游戏", "软件", "学习", "其他"]) {
|
|
const p = parseRelease(`[${c}]Some Title S01 2026 1080p WEB-DL-GRP`);
|
|
expect(p.category, c).toBe(c);
|
|
expect(p.titleCn, c).toBeUndefined();
|
|
}
|
|
});
|
|
it("数字紧跟普通单词是片名的一部分,不是集号(`Travis - 12 Memories`)", () => {
|
|
const p = parseRelease("[音乐]Travis - 12 Memories (2003) - WEB-DL - 16bit ALAC-HHWEB");
|
|
expect(p.episodes).toBeUndefined();
|
|
expect(p.titleEn).toContain("12 Memories");
|
|
expect(parseRelease("[Sub] Some Anime - 12 [1080p]").episodes).toEqual({ start: 12, end: 12 });
|
|
expect(parseRelease("Some Anime - 05 END [1080p]").episodes).toEqual({ start: 5, end: 5 });
|
|
});
|
|
it("副标题第一段:主标题没有中文名时取第一个中文名,其余进 altTitles;字幕/简介字段不算片名", () => {
|
|
const p = parseRelease("Archives S01 2009 1080p WEB-DL H.264 AAC-HHWEB", {
|
|
subtitle: "档案 / 档案解密 | 第152集 | 1080p | 类型: 纪录片/历史 | 主演: 石凉 [内嵌中字]",
|
|
});
|
|
expect(p).toMatchObject({ titleCn: "档案", titleEn: "Archives", altTitles: ["档案解密"], year: 2009 });
|
|
const q = parseRelease("Interstellar 2014 2160p UHD Blu-ray-GRP", {
|
|
subtitle: "星际穿越 IMAX版/星際啟示錄(港)/英语DTS-HD MA5.1/原盘英简繁&双语特效SUP字幕/章节",
|
|
});
|
|
expect(q.titleCn).toBe("星际穿越");
|
|
expect(q.altTitles).toEqual(["星際啟示錄"]);
|
|
expect(parseRelease("Some Movie 2020 1080p-GRP", { subtitle: "官方中字 | 特效字幕" }).titleCn).toBeUndefined();
|
|
});
|
|
it("通用词不进比对名称", () => {
|
|
expect(isGenericAliasName(normalizeTitle("电视剧"))).toBe(true);
|
|
expect(isGenericAliasName("剧")).toBe(true);
|
|
expect(isGenericAliasName("tv")).toBe(true);
|
|
expect(isGenericAliasName(normalizeTitle("档案"))).toBe(false);
|
|
const p = parseRelease("Lanterns 2026 1080p-GRP");
|
|
expect(queryTitles({ ...p, titleCn: "电视剧" })).toEqual(["lanterns"]);
|
|
});
|
|
});
|
|
|
|
describe("续作 / 章节序号", () => {
|
|
it("Chapter / Part / 第N章 / 罗马数字符号", () => {
|
|
expect(sequelNumbers("Mononoke The Movie Chapter III The Curse of the Serpent")).toEqual([3]);
|
|
expect(sequelNumbers("剧场版物怪:第 3 章:蛇神")).toEqual([3]);
|
|
expect(sequelNumbers("劇場版モノノ怪 第三章 蛇神")).toEqual([3]);
|
|
expect(sequelNumbers("Mononoke the Movie: Chapter Ⅱ")).toEqual([2]);
|
|
expect(sequelNumbers("Dune Part Two")).toEqual([2]);
|
|
expect(sequelNumbers("Blade Runner 2049")).toEqual([]);
|
|
});
|
|
it("序号冲突直接拒绝,一致加分", () => {
|
|
const p = parseRelease("Mononoke The Movie Chapter III The Curse of the Serpent 2026 1080p NF WEB-DL-HHWEB");
|
|
const c3: IdentityCandidate = {
|
|
provider: "tmdb",
|
|
entityType: "movie",
|
|
externalId: "3",
|
|
titles: ["剧场版物怪:第 3 章:蛇神"],
|
|
aliases: ["Mononoke the Movie: Chapter III - The Curse of the Serpent"],
|
|
year: 2026,
|
|
};
|
|
const c2 = {
|
|
...c3,
|
|
externalId: "2",
|
|
titles: ["剧场版物怪:第 2 章:火鼠"],
|
|
aliases: ["Mononoke the Movie: Chapter II"],
|
|
};
|
|
expect(scoreCandidate(p, c3).signals.sequel).toBe("match");
|
|
expect(scoreCandidate(p, c2).level).toBe("rejected");
|
|
expect(resolveIdentity(p, [c2, c3]).best?.candidate.externalId).toBe("3");
|
|
});
|
|
});
|
|
|
|
describe("证据:中英名、通用词、别名佐证、同级取舍", () => {
|
|
it("中文名精确 + 年份精确 → rule_confirmed;唯一候选 → auto", () => {
|
|
const p = parseRelease("Martial Master S01 2020 1080p WEB-DL-HHWEB", { subtitle: "武神主宰 | 第685集" });
|
|
const r = resolveIdentity(p, [tv("110181", ["武神主宰"], 2020), tv("228707", ["武神主宰"], 2023)]);
|
|
expect(r).toMatchObject({ status: "auto", best: { level: "rule_confirmed" } });
|
|
expect(r.best?.candidate.externalId).toBe("110181");
|
|
});
|
|
it("通用英文词只靠别名命中且池里撞名 → 封顶 needs_confirm;中文名命中则确认", () => {
|
|
const noCn = parseRelease("Archives S01 2009 1080p WEB-DL-HHWEB");
|
|
const pool = [
|
|
tv("201401", ["档案"], 2009, { aliases: ["Archives"] }),
|
|
tv("1", ["Archives secrètes"], 2021),
|
|
tv("2", ["Mystères d'archives"], 2009),
|
|
tv("3", ["The Magnus Archives"], 2016),
|
|
];
|
|
const r = resolveIdentity(noCn, pool);
|
|
expect(r.status).toBe("ambiguous");
|
|
expect(r.best?.evidence).toContain("generic_name");
|
|
const withCn = parseRelease("Archives S01 2009 1080p WEB-DL-HHWEB", { subtitle: "档案 | 第152集" });
|
|
expect(resolveIdentity(withCn, pool)).toMatchObject({
|
|
status: "auto",
|
|
best: { candidate: { externalId: "201401" } },
|
|
});
|
|
});
|
|
it("别名命中需要年份或第二种文字的名称佐证", () => {
|
|
const p = parseRelease("人中之龙第一季 1080p WEB-DL H265 AAC-HHWEB", {
|
|
subtitle: "人中之龙第一季 | 全98集 | 1080p",
|
|
});
|
|
const r = scoreCandidate(p, tv("256116", ["如龙", "龍が如く"], 2024, { aliases: ["人中之龙"] }));
|
|
expect(r.level).toBe("needs_confirm");
|
|
expect(r.evidence).toContain("alias_uncorroborated");
|
|
});
|
|
it("同级多候选:年份精确优先,再按命中的名称文字种数", () => {
|
|
const p = parseRelease("A Prophet S01 2026 2160p TX WEB-DL-HHWEB", { subtitle: "我不是大师 / 长风起 | 第05集" });
|
|
const right = tv("289078", ["我不是大师"], 2026, { aliases: ["A Prophet"] });
|
|
const other = tv("224006", ["预言者", "Un prophète", "A Prophet"], 2026);
|
|
const r = resolveIdentity(p, [other, right]);
|
|
expect(r.status).toBe("auto");
|
|
expect(r.best?.candidate.externalId).toBe("289078");
|
|
expect(r.reason).toBe("rule_confirmed:tie_break");
|
|
// 完全对称 → 仍进池
|
|
expect(resolveIdentity(p, [tv("1", ["我不是大师"], 2026), tv("2", ["我不是大师"], 2026)]).status).toBe("ambiguous");
|
|
});
|
|
it("季号不在候选季列表里扣分", () => {
|
|
const p = parseRelease("Heart Signal S09 2018 1080p WEB-DL-HHWEB");
|
|
const r = scoreCandidate(p, tv("72976", ["Heart Signal"], 2018, { seasonNumbers: [1, 2, 3, 4] }));
|
|
expect(r.evidence).toContain("season:missing");
|
|
expect(r.level).toBe("needs_confirm");
|
|
});
|
|
it("权重表可覆盖", () => {
|
|
const p = parseRelease("Oppenheimer 2023 1080p-GRP");
|
|
const c: IdentityCandidate = {
|
|
provider: "tmdb",
|
|
entityType: "movie",
|
|
externalId: "1",
|
|
titles: ["Oppenheimer"],
|
|
year: 2023,
|
|
aliases: [],
|
|
};
|
|
expect(scoreCandidate(p, c).level).toBe("rule_confirmed");
|
|
expect(
|
|
scoreCandidate(p, c, { weights: { thresholds: { ...DEFAULT_MATCH_WEIGHTS.thresholds, rule_confirmed: 9 } } })
|
|
.level,
|
|
).toBe("multi_evidence");
|
|
});
|
|
});
|