fix(identity): persist verified auxiliary season mappings and reuse source ids

This commit is contained in:
root committed 2026-10-13 20:35:51 +08:00
1 parent c85b139ec4
commit ee0d62ae6f
15 files changed
+397 -38

No files matched your search

@@ -0,0 +1,24 @@
import type { Kysely } from "kysely";
import { execDdl } from "../src/migration-helpers.ts";
// Bangumi/豆瓣按分季建条目,一个 TMDB 作品可关联多个来源 ID;来源 ID 仍全局唯一。
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`drop index external_identity_entity_accepted_key;
create unique index external_identity_entity_accepted_key on external_identity (media_entity_id, provider, entity_type)
where status = 'accepted' and media_entity_id is not null and provider not in ('bangumi', 'douban');`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
// 已有多分季映射时唯一索引重建会拒绝回退,不删除或猜测保留哪条身份。
await execDdl(
db,
`drop index external_identity_entity_accepted_key;
create unique index external_identity_entity_accepted_key on external_identity (media_entity_id, provider, entity_type)
where status = 'accepted' and media_entity_id is not null;`,
);
}
@@ -647,6 +647,12 @@ function walkTokens(tokens: string[], d: Draft): void {
stop("complete");
continue;
}
if ((u === "BATCH" || u === "合集" || u === "全集") && d.episodes) {
d._complete = true;
ev.push("pack:batch");
stop("complete");
continue;
}
if ((u === "FIN" || u === "END") && d.episodes) {
d._fin = true;
ev.push("pack:fin");
@@ -1017,6 +1023,13 @@ export function parseRelease(input: string, opts: ParseOptions = {}): ParsedRele
ev.push("pack:fin");
return MARK_HARD;
});
// BT 动漫明确区间:名称后的「 - 01-12」不是片名,Batch/合集标记另决定是否整季包。
s = s.replace(/\s-\s(\d{1,3})(?:v\d)?\s*[-~]\s*(\d{1,3})(?:v\d)?(?=\s|\[|【|$)/u, (all, a: string, b: string) => {
if (d.episodes || Number(b) < Number(a)) return all;
d.episodes = { start: Number(a), end: Number(b) };
ev.push("episode:anime_dash_range");
return MARK_HARD;
});
// `Travis - 12 Memories`:数字后紧跟一个普通单词时是片名的一部分,不是集号
s = s.replace(
/\s-\s(\d{1,3})(?:v\d)?(?=\s|\[|【|$)(?!\s+(?!(?:END|Fin|Final|RAW)\b)[A-Za-z][a-z]+\b)/u,
+1 -1
View File
@@ -2,7 +2,7 @@ import type { ContentType } from "@umt/contracts";
import type { CompiledGlobalWord } from "../identify-words/apply.ts";
/** 解析器版本:规则变化时递增,缓存/识别结果按此失效 */
export const PARSER_VERSION = 2;
export const PARSER_VERSION = 3;
export type Resolution = "4320p" | "2160p" | "1440p" | "1080p" | "1080i" | "720p" | "576p" | "480p";
+31 -1
View File
@@ -5,6 +5,7 @@ import {
expandEpisodes,
fileKindOf,
OffsetExpressionError,
PARSER_VERSION,
type ParsedRelease,
parseRelease,
parseReleasePath,
@@ -742,7 +743,7 @@ describe("parseRelease 样本表", () => {
it(`${s.id} ${s.input}${s.deviation ? ` [deviation: ${s.deviation}]` : ""}`, () => {
const r = parseRelease(s.input, s.subtitle ? { subtitle: s.subtitle } : {});
check(r, s.expect);
expect(r.parserVersion).toBe(2);
expect(r.parserVersion).toBe(PARSER_VERSION);
expect(r.raw).toBe(s.input);
});
}
@@ -936,3 +937,32 @@ describe("辅助函数", () => {
expect(parseRelease("12").episodes).toBeUndefined();
});
});
// BT 发布的原编号保持来源事实;不根据作品季度或分辨率改写。
describe("BT 动漫复杂命名", () => {
it("dash 范围和 Batch 合集", () => {
const parsed = parseRelease("[SubsPlease] Dr. Stone - 01-12 [1080p] [Batch]");
expect(parsed.title).toBe("Dr Stone");
expect(parsed.episodes).toEqual({ start: 1, end: 12 });
expect(parsed.packHint).toBe("season_pack");
expect(parsed.evidence).toContain("episode:anime_dash_range");
});
it("方括号范围加中文合集", () => {
const parsed = parseRelease("[Group][葬送的芙莉莲][01-28][1080p][合集]");
expect(parsed.titleCn).toBe("葬送的芙莉莲");
expect(parsed.episodes).toEqual({ start: 1, end: 28 });
expect(parsed.packHint).toBe("season_pack");
});
it("双语、分季、v2 与发布组保留", () => {
const parsed = parseRelease(
"[喵萌奶茶屋] 药屋少女的呢喃 第二季 / Kusuriya no Hitorigoto 2nd Season - 05v2 [1080p][HEVC-10bit][简繁内封]",
);
expect(parsed.titleCn).toBe("药屋少女的呢喃");
expect(parsed.season).toBe(2);
expect(parsed.episodes).toEqual({ start: 5, end: 5 });
expect(parsed.releaseGroup?.name).toBe("喵萌奶茶屋");
});
it("小数特别集不猜成普通集", () => {
expect(parseRelease("[Group] Sousou no Frieren - 05.5 [1080p]").episodes).toBeUndefined();
});
});