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
This commit is contained in:
1 parent
12f8fc59c3
commit
63e94b308e
90 files changed
+6366
-2131
No files matched your search
@@ -3,6 +3,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { reresolveEntityEpisodes } from "../../src/modules/sources-identity/service/episode-resolution.ts";
|
||||
import { refreshMetadata } from "../../src/modules/sources-identity/service/metadata.ts";
|
||||
import { resolveRelease } from "../../src/modules/sources-identity/service/resolve.ts";
|
||||
import { setWordsText } from "../support/identify-words.ts";
|
||||
import { createHarness, type Harness, pgAvailable, seedSite } from "./harness.ts";
|
||||
|
||||
// 集数解析接入识别(docs/plan/episode-resolution.md 批次 2):识别绑定作品后按季表 / 规则换算季集再建单元;
|
||||
@@ -91,6 +92,7 @@ describe.skipIf(!pg)("集数解析接入识别", () => {
|
||||
expect(res.outcome, JSON.stringify(res)).toBe("auto");
|
||||
return { id: r.id, res, row: await row(r.id) };
|
||||
};
|
||||
const setWords = (entityId: string, text: string) => setWordsText(h.db, entityId, text);
|
||||
const row = (id: string) =>
|
||||
h.db.selectFrom("source_release").selectAll().where("id", "=", id).executeTakeFirstOrThrow();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 测试断言读 jsonb
|
||||
@@ -201,71 +203,41 @@ describe.skipIf(!pg)("集数解析接入识别", () => {
|
||||
expect(ds[0]!.summary).toContain("共 2 条资源");
|
||||
});
|
||||
|
||||
it("有 active 规则 `EP-12`(季 2、组 ANi)→ rule;规则算出越界 → 规则停用并开待处理", async () => {
|
||||
const rule = await h.db
|
||||
.insertInto("episode_mapping_rule")
|
||||
.values({
|
||||
media_entity_id: kId,
|
||||
season: 2,
|
||||
release_group: "ANi",
|
||||
kind: "expression",
|
||||
expression: "EP-12",
|
||||
created_by_type: "user",
|
||||
created_by_id: "admin",
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
// 同作用域(发布组忽略大小写)只能有一条 active
|
||||
await expect(
|
||||
h.db
|
||||
.insertInto("episode_mapping_rule")
|
||||
.values({
|
||||
media_entity_id: kId,
|
||||
season: 2,
|
||||
release_group: "ani",
|
||||
kind: "expression",
|
||||
expression: "EP",
|
||||
created_by_type: "user",
|
||||
})
|
||||
.execute(),
|
||||
).rejects.toThrow();
|
||||
|
||||
it("作品识别词 `S02 @ANi >> EP-12` → rule;算出越界 → 待确认,这一行照常生效(不自动停用)", async () => {
|
||||
await setWords(kId, "# 第二季接着第一季编号\nS02 @ANi >> EP-12\n");
|
||||
const ok = await resolve("[ANi] Kusuriya no Hitorigoto S02 - 13 [1080P][Baha][WEB-DL]");
|
||||
expect(er(ok.row)).toMatchObject({
|
||||
method: "rule",
|
||||
ruleId: rule.id,
|
||||
ruleLine: 2,
|
||||
ruleText: "S02 @ANi >> EP-12",
|
||||
canonical: { season: 2, episodes: { start: 1, end: 1 } },
|
||||
});
|
||||
expect(await unitOf(ok.row.content_unit_id)).toMatchObject({ season_number: 2, episode_number: 1 });
|
||||
|
||||
const bad = await resolve("[ANi] Kusuriya no Hitorigoto S02 - 30 [1080P][Baha][WEB-DL]");
|
||||
expect(er(bad.row)).toMatchObject({ method: "unresolved", ruleFailed: rule.id });
|
||||
expect(er(bad.row)).toMatchObject({ method: "unresolved", ruleText: "S02 @ANi >> EP-12" });
|
||||
expect(er(bad.row)).not.toHaveProperty("ruleFailed");
|
||||
expect(bad.row.content_unit_id).toBeNull();
|
||||
const after = await h.db
|
||||
.selectFrom("episode_mapping_rule")
|
||||
.selectAll()
|
||||
.where("id", "=", rule.id)
|
||||
// 识别词原文没被改动
|
||||
const w = await h.db
|
||||
.selectFrom("identify_words")
|
||||
.select(["text", "version"])
|
||||
.where("media_entity_id", "=", kId)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(after.status).toBe("disabled");
|
||||
expect(after.disabled_reason).toContain("rule:validate_fail");
|
||||
expect(w.text).toBe("# 第二季接着第一季编号\nS02 @ANi >> EP-12\n");
|
||||
const d = (await openEpisodeDecisions()).find((x) => x.dedupe_key === `episode_mapping:${kId}:2:ani`);
|
||||
expect(d?.evidence).toMatchObject({ sourceReleaseIds: [bad.id], failedRules: [rule.id] });
|
||||
expect(d?.summary).toContain("规则已停用");
|
||||
expect(d?.evidence).toMatchObject({ sourceReleaseIds: [bad.id] });
|
||||
expect(d?.evidence).not.toHaveProperty("failedRules");
|
||||
expect(d?.summary).toContain("按作品识别词「S02 @ANi >> EP-12」算出的集数对不上作品季表");
|
||||
expect(d?.summary).not.toContain("停用");
|
||||
|
||||
// 越界之后同一行对新资源照样生效
|
||||
const again = await resolve("[ANi] Kusuriya no Hitorigoto S02 - 14 [1080P][Baha][WEB-DL]");
|
||||
expect(er(again.row)).toMatchObject({ method: "rule", canonical: { season: 2, episodes: { start: 2, end: 2 } } });
|
||||
});
|
||||
|
||||
it("新增规则后按作品重算:之前待确认的资源解决、建出单元、待处理关闭", async () => {
|
||||
await h.db
|
||||
.insertInto("episode_mapping_rule")
|
||||
.values({
|
||||
media_entity_id: kId,
|
||||
season: 1,
|
||||
release_group: "Lilith-Raws",
|
||||
kind: "expression",
|
||||
expression: "EP-30",
|
||||
target_season: 2,
|
||||
created_by_type: "user",
|
||||
})
|
||||
.execute();
|
||||
await setWords(kId, "S02 @ANi >> EP-12\nS01 @Lilith-Raws >> S02 EP-30\n");
|
||||
const decisionId = (await openEpisodeDecisions()).find((x) => x.dedupe_key.endsWith(":1:lilith-raws"))!.id;
|
||||
const r = await h.db.transaction().execute((trx) => reresolveEntityEpisodes(h.ctx, trx, kId));
|
||||
expect(r.resolved).toBe(2);
|
||||
|
||||
Reference in new issue
Block a user