Files
rootandClaude Opus 5.5 63e94b308e feat(identify-words): 识别词——全局识别词(MP 语法)+ 作品识别词(季集换算),规则页「识别词」页签
- 新表 identify_words(0094):全局一块 + 每部作品一块原文,带版本与审计;episode_mapping_rule 转成文本行后删表
- 全局识别词移植 MP WordsMatcher(屏蔽 / 替换 / 集数偏移 / 替换+偏移 / {[tmdbid=…]}),所有按标题 / 文件名识别的地方解析前套用,命中的行记在解析结果上;保存后后台按改动行重新识别
- 作品识别词:S02 >> S01 EP+24 / S01E13 >> S00E01 / S01 @组 >> 忽略,解析器 + 打印器 + 编译;发布组繁简视为同一个
- 去掉规则自动停用:算出来对不上季表只待确认,要停用就注释掉那一行
- 集数待确认的采用 / 忽略与智能体 apply_rule 改为往作品识别词追加一行(带来历注释)
- 编辑器可视化 / 文本两种模式改同一份原文,保存前试算;作品页「集数映射」用同一个编辑器

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-08 17:03:00 +08:00

95 lines
3.7 KiB
TypeScript

import type { EpisodeMappingPreview } from "@umt/contracts";
import { describe, expect, it } from "vitest";
import { EXCLUDE_REASON } from "../organize-wizard/model.ts";
import {
absoluteGroups,
canonicalText,
groupMethodsText,
lineFromSearch,
METHOD_LABEL,
mappingHref,
methodText,
reresolveToast,
sourceText,
} from "./model.ts";
describe("集数映射文案", () => {
it("方式名称给人话,覆盖全部方式", () => {
expect(METHOD_LABEL).toEqual({
raw: "原号",
rule: "按作品识别词",
absolute_in_season: "季内绝对集号",
absolute_cross_season: "跨季绝对集号",
air_date: "按播出日期",
tmdb_season: "按 TMDB 默认季集换算",
unverified: "元数据未覆盖,先按原号",
unresolved: "待确认",
skipped: "不适用",
});
expect(methodText({ method: "rule", special: "ignore" })).toBe("按作品识别词:忽略");
expect(methodText(null)).toBe("未解析");
});
it("季集写法:原始 / 规范 / 特别篇 / 播出日期", () => {
expect(sourceText({ source: { season: 1, episodes: { start: 25, end: 25 } } })).toBe("S01E25");
expect(sourceText({ source: { episodes: { start: 13, end: 14 } } })).toBe("E13-E14");
expect(sourceText({ source: { airDate: "20250314" } })).toBe("2025-03-14");
expect(
canonicalText({ method: "absolute_cross_season", canonical: { season: 2, episodes: { start: 1, end: 1 } } }),
).toBe("S02E01");
expect(canonicalText({ method: "rule", canonical: null, special: "extra" })).toBe("花絮");
expect(canonicalText({ method: "unresolved", canonical: null })).toBe("—");
});
it("整理向导的排除原因有「集数待确认」与特典花絮", () => {
expect(EXCLUDE_REASON.episode_unresolved).toBe("集数待确认");
expect(EXCLUDE_REASON.special_extra).toBeTruthy();
});
});
describe("作品页地址", () => {
it("集数待确认「修改」带上要追加的一行,作品页能读回", () => {
const href = mappingHref("/console/library/x", "S01 @ANi >> S02 EP-12");
expect(href).toBe("/console/library/x?mapLine=S01+%40ANi+%3E%3E+S02+EP-12#episode-mapping");
const sp = new URLSearchParams(href.slice(href.indexOf("?") + 1, href.indexOf("#")));
expect(lineFromSearch(sp)).toBe("S01 @ANi >> S02 EP-12");
expect(lineFromSearch(new URLSearchParams())).toBeNull();
expect(mappingHref("/console/library/x")).toBe("/console/library/x#episode-mapping");
});
});
describe("按发布组统计", () => {
const preview = {
groups: [
{
releaseGroup: "ANi",
total: 5,
pending: 0,
suggestion: null,
methods: [
{ method: "raw", count: 1 },
{ method: "absolute_cross_season", count: 4 },
],
},
{ releaseGroup: null, total: 1, pending: 1, suggestion: null, methods: [{ method: "unresolved", count: 1 }] },
],
} as unknown as EpisodeMappingPreview;
it("一句话统计按数量排序", () => {
expect(groupMethodsText(preview.groups[0]!)).toBe("ANi:跨季绝对集号 4、原号 1");
expect(groupMethodsText(preview.groups[1]!)).toBe("未知发布组:待确认 1");
});
it("找出用绝对集号的发布组", () => {
expect(absoluteGroups(preview)).toEqual(["ANi"]);
expect(absoluteGroups(undefined)).toEqual([]);
});
});
describe("保存后的提示", () => {
it("只列非零项", () => {
expect(reresolveToast({ releases: 12, changed: 2, resolved: 2 })).toBe(
"已保存:重算 12 条资源,2 条有变化,2 条待确认已解决",
);
expect(reresolveToast({ releases: 3, changed: 0, resolved: 0 }, "已写入")).toBe("已写入:重算 3 条资源");
});
});