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]>
76 lines
3.6 KiB
TypeScript
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);
|
|
});
|
|
});
|