Files
rootandClaude Opus 5.5 387f7931e0 feat(sources): MP-aligned TMDB recall, alias registration guard, re-resolve on retitle (ID-2)
recallTmdbCandidates searches cn then en names in the preferred language by
type and fetches details for a bounded set of candidates (MP alias fallback);
translations become aliases; bindAuto no longer registers generic or
alias-only site titles; snapshot/feed title changes reparse and force a
re-resolve that unbinds when it can no longer confirm.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 17:41:22 +08:00

76 lines
3.6 KiB
TypeScript

/**
* ID-2:站点资源标题更新(RSS 带分类前缀 / 内嵌描述的标题 → 列表标题)后按新标题重新解析并重新识别,
* 不沿用旧解析;已识别的资源也不短路。
*/
import type { SiteRelease } from "@umt/adapters";
import { PARSER_VERSION } from "@umt/domain";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { upsertSiteRelease } from "../../src/modules/sources-identity/service/snapshot.ts";
import { sourcesOutboxRoutes } from "../../src/modules/sources-identity/tasks.ts";
import { createHarness, type Harness, pgAvailable, seedSite } from "./harness.ts";
const up = await pgAvailable();
const rel = (title: string, subtitle?: string): SiteRelease => ({
siteId: "x",
siteReleaseId: "9001",
title,
...(subtitle ? { subtitle } : {}),
category: { id: "402", name: "剧集" },
sizeBytes: null,
seeders: null,
leechers: null,
snatched: null,
promotion: { type: "none", downloadFactor: 1, uploadFactor: 1, expiresAt: null } as SiteRelease["promotion"],
hr: false,
uploadedAt: null,
detailUrl: "https://hhan.example/details.php?id=9001",
tags: [],
raw: {},
observedAt: new Date().toISOString(),
});
describe.skipIf(!up)("标题更新后重新解析 + 重新识别", () => {
let h: Harness;
let siteId: string;
beforeAll(async () => {
h = await createHarness("retitle");
siteId = (await seedSite(h, { baseUrl: "https://hhan.example/" })).site.id;
});
afterAll(async () => {
await h?.close();
});
it("RSS 标题 → 列表标题:parsed 按新标题重算,发 release.observed(isNew=false)并强制重新识别", async () => {
const rss =
"[电视剧]Irreplaceable S01 2026 1080p YK WEB-DL H.264 DTS5.1-HHWEB[无可替代 | 第03集 | 1080p | 类型: 剧情][510.37 MB]";
const first = await h.db.transaction().execute((trx) => upsertSiteRelease(h.rt, trx, { id: siteId }, rel(rss)));
expect(first.inserted).toBe(true);
let row = await h.db.selectFrom("source_release").selectAll().where("id", "=", first.id).executeTakeFirstOrThrow();
// 分类前缀不进片名;内嵌描述里的中文名补作中文名
expect(row.parsed).toMatchObject({ titleCn: "无可替代", titleEn: "Irreplaceable", category: "电视剧" });
const listTitle = "Irreplaceable S01 2026 1080p YK WEB-DL H.264 DTS5.1-HHWEB";
const again = await h.db
.transaction()
.execute((trx) => upsertSiteRelease(h.rt, trx, { id: siteId }, rel(listTitle, "无可替代 | 第04集 | 1080p")));
expect(again).toEqual({ id: first.id, inserted: false });
row = await h.db.selectFrom("source_release").selectAll().where("id", "=", first.id).executeTakeFirstOrThrow();
expect(row.title).toBe(listTitle);
expect(row.parser_version).toBe(String(PARSER_VERSION));
expect(row.parsed).toMatchObject({ titleCn: "无可替代", episodes: { start: 4, end: 4 } });
const events = (await h.outbox("release.observed")).filter((e) => e.payload.sourceReleaseId === first.id);
expect(events.map((e) => e.payload.isNew)).toEqual([true, false]);
const route = sourcesOutboxRoutes["release.observed"]!({ payload: events[1]!.payload } as never);
expect(route).toMatchObject({ task: "resolveObservation", input: { force: true, trigger: "observed" } });
// 同标题再次快照:不再发重新识别
await h.db
.transaction()
.execute((trx) => upsertSiteRelease(h.rt, trx, { id: siteId }, rel(listTitle, "无可替代 | 第04集 | 1080p")));
const after = (await h.outbox("release.observed")).filter((e) => e.payload.sourceReleaseId === first.id);
expect(after).toHaveLength(2);
});
});