- 新表 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
55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
Api,
|
|
EpisodeMappingPreview,
|
|
EpisodeMappingPreviewInput,
|
|
EpisodeResolutionView,
|
|
listEndpoints,
|
|
} from "../src/index.ts";
|
|
|
|
// 集数映射与识别词接口的契约:端点登记、预览入参、解析结果结构。
|
|
|
|
describe("集数映射契约", () => {
|
|
it("端点登记在 Api 注册表:读和试算用 read,作品识别词写 organize.execute,全局写 admin", () => {
|
|
const keys = listEndpoints()
|
|
.filter((e) => e.path.includes("episode-mapping") || e.path.includes("identify-words"))
|
|
.map((e) => `${e.method.toUpperCase()} ${e.path} ${e.scope}`)
|
|
.sort();
|
|
expect(keys).toEqual([
|
|
"GET /identify-words/entities read",
|
|
"GET /identify-words/global read",
|
|
"GET /media-entities/{id}/identify-words read",
|
|
"POST /identify-words/global/dry-run read",
|
|
"POST /identify-words/try read",
|
|
"POST /media-entities/{id}/episode-mapping/preview read",
|
|
"POST /media-entities/{id}/identify-words/dry-run read",
|
|
"PUT /identify-words/global admin",
|
|
"PUT /media-entities/{id}/identify-words organize.execute",
|
|
]);
|
|
expect(Api.episodeMapping.preview.response).toBe(EpisodeMappingPreview);
|
|
});
|
|
|
|
it("预览入参:草稿是整块识别词原文,条数上限 300", () => {
|
|
expect(EpisodeMappingPreviewInput.parse({})).toEqual({});
|
|
expect(
|
|
EpisodeMappingPreviewInput.safeParse({ season: 2, releaseGroup: "ANi", words: "S02 >> S01 EP+24" }).success,
|
|
).toBe(true);
|
|
expect(EpisodeMappingPreviewInput.safeParse({ limit: 301 }).success).toBe(false);
|
|
});
|
|
|
|
it("解析结果:与 source_release.episode_resolution 同结构", () => {
|
|
const v = {
|
|
method: "absolute_cross_season",
|
|
source: { season: 1, episodes: { start: 25, end: 25 } },
|
|
canonical: { season: 2, episodes: { start: 13, end: 13 } },
|
|
evidence: ["conversion:cross_season"],
|
|
};
|
|
expect(EpisodeResolutionView.parse(v)).toEqual(v);
|
|
expect(EpisodeResolutionView.safeParse({ ...v, method: "guess" }).success).toBe(false);
|
|
expect(
|
|
EpisodeResolutionView.safeParse({ method: "rule", source: {}, canonical: null, special: "ignore", evidence: [] })
|
|
.success,
|
|
).toBe(true);
|
|
});
|
|
});
|