import { workNfo } from "@umt/domain"; import { createMockTmdb } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { imagePathsOf, nfoWork } from "../../src/modules/acquisition-organize/services/scrape-input.ts"; import { saveDetailPage } from "../../src/modules/sources-identity/service/detail-page.ts"; import { ptDetailFacts, resolveOrganizeMetadata, } from "../../src/modules/sources-identity/service/organize-metadata.ts"; import { createHarness, type Harness, pgAvailable, seedSite } from "./harness.ts"; const ptText = "◎译 名 未收录的故事\n◎原 名 Unlisted Story\n◎年 代 2025\n◎产 地 中国大陆\n◎语 言 汉语普通话\n◎类 别 剧情\n◎导 演 张三\n◎主 演 李四 / 王五\n◎简 介 一位主人公在旅途中发现了一段不为人知的故事,并开始寻找失踪的朋友。"; const up = await pgAvailable(); describe("PT detail extraction", () => { it("reads evidenced labels and leaves missing fields empty", () => { const facts = ptDetailFacts(ptText, "https://pt.example/details.php?id=1"); expect(facts.names).toEqual(["未收录的故事", "Unlisted Story"]); expect(facts.year).toBe(2025); expect(facts.extras).toContainEqual({ field: "originCountry", value: ["CN"] }); expect(facts.extras).toContainEqual({ field: "originalLanguage", value: "zh" }); expect(ptDetailFacts("Unknown.2025.1080p", "https://pt.example/").candidates).toEqual([]); }); it("local final fields reuse NFO input and external poster", () => { const metadata = { overview: { value: "简介" }, posterUrl: { value: "https://img.example/cover.jpg" }, originCountry: { value: ["CN"] }, cast: { value: [{ id: -1, name: "本地主演", role: null, profilePath: null }] }, crew: { value: [{ id: -2, name: "本地导演", role: "Director", profilePath: null }] }, }; expect(imagePathsOf(metadata).posterPath).toBe("https://img.example/cover.jpg"); const nfo = nfoWork({ kind: "movie", title: "本地作品", year: 2025, original_title: null, metadata }, {}); expect(nfo.ids.tmdb).toBeNull(); expect(nfo.overview).toBe("简介"); expect(nfo.countries).toEqual(["中国"]); expect(nfo.actors?.[0]?.tmdbId).toBeNull(); expect(nfo.directors?.[0]?.tmdbId).toBeNull(); const xml = workNfo(nfo); expect(xml).toContain("本地主演"); expect(xml).toContain("本地导演"); expect(xml).not.toContain("-1"); expect(xml).not.toContain("-2"); }); }); describe.skipIf(!up)("organize metadata local recovery (isolated PG)", () => { let h: Harness; let siteId: string; beforeAll(async () => { h = await createHarness("local_metadata", { tmdb: () => null }); siteId = (await seedSite(h, { baseUrl: "https://pt.example/" })).site.id; }); afterAll(async () => { await h?.close(); }); const add = async (n: string, text: string, title?: string) => { const release = await h.db .insertInto("source_release") .values({ site_id: siteId, site_release_id: n, title: title ?? `Unlisted Story ${n} 2025 1080p`, detail_fetched_at: new Date(), }) .returning("id") .executeTakeFirstOrThrow(); await saveDetailPage( h.rt, h.db, release.id, { descriptionText: text, mediaInfo: null, screenshots: ["https://img.example/cover.jpg"], imdbId: null, doubanId: null, tmdb: null, files: [], tags: [], uploader: null, probe: { labels: [], hasDescription: true, imageCount: 1 }, }, 3600, ); return release.id; }; const deps = { search: async () => [], fetch: async () => { throw new Error("unexpected fetch"); }, complete: async () => null, }; it("creates Local with sourced fields, remains revisitable and reuses identity", async () => { const id = await add("1", ptText); const out = await resolveOrganizeMetadata(h.rt, id, deps, { actor: { type: "system", id: "system:organize" } }); expect(out.mode).toBe("local"); expect(out.mediaEntityId).toBeTruthy(); const entity = await h.db .selectFrom("media_entity") .selectAll() .where("id", "=", out.mediaEntityId!) .executeTakeFirstOrThrow(); expect(entity.status).toBe("provisional"); expect(entity.title).toBe("未收录的故事"); expect((entity.metadata as Record).originCountry?.value).toEqual(["CN"]); expect((entity.metadata as Record).posterUrl?.value).toBe( "https://img.example/cover.jpg", ); const release = await h.db.selectFrom("source_release").selectAll().where("id", "=", id).executeTakeFirstOrThrow(); expect(release.resolution_locked).toBe(false); const again = await resolveOrganizeMetadata(h.rt, id, deps, { actor: { type: "system", id: "system:organize" } }); expect(again.mediaEntityId).toBe(out.mediaEntityId); }); it("series fallback preserves actual season and episode instead of fabricating season one", async () => { const id = await add( "season", "◎译 名 独立剧集\n◎年 代 2025\n◎集 数 12\n◎类 别 剧情", "Distinct Local Series S02E03 2025 1080p", ); const out = await resolveOrganizeMetadata(h.rt, id, deps, { actor: { type: "system", id: "system:organize" } }); expect(out.mode).toBe("local"); const units = await h.db .selectFrom("content_unit") .select(["season_number", "episode_number"]) .where("media_entity_id", "=", out.mediaEntityId!) .execute(); expect(units.some((u) => u.season_number === 1)).toBe(false); expect(units.some((u) => u.season_number === 2 && u.episode_number === 3)).toBe(true); }); it("does not create a filename placeholder when facts are insufficient", async () => { const id = await add("2", "no metadata available"); const out = await resolveOrganizeMetadata(h.rt, id, deps, { actor: { type: "system", id: "system:organize" } }); expect(out.mediaEntityId).toBeNull(); }); }); describe.skipIf(!up)("organize metadata translated name recovery", () => { it("retries TMDB using evidenced PT aliases after normal title lookup fails", async () => { const tmdb = await createMockTmdb({ fixtures: [ { type: "movie", id: 990087, original_title: "True Original", original_language: "en", origin_country: ["US"], date: "2025-01-01", titles: { "zh-CN": "正确译名", "en-US": "True Original" }, popularity: 10, alternative_titles: [], external_ids: { imdb_id: null }, }, ], }); const h = await createHarness("local_alias", { tmdbUrl: tmdb.url }); try { const siteId = (await seedSite(h, { baseUrl: "https://pt.example/" })).site.id; const release = await h.db .insertInto("source_release") .values({ site_id: siteId, site_release_id: "alias", title: "Unknown Translation 2025 1080p", detail_fetched_at: new Date(), }) .returning("id") .executeTakeFirstOrThrow(); await saveDetailPage( h.rt, h.db, release.id, { descriptionText: "◎译 名 另类译名\n◎原 名 True Original\n◎年 代 2025\n◎类 别 剧情", mediaInfo: null, screenshots: [], imdbId: null, doubanId: null, tmdb: null, files: [], tags: [], uploader: null, probe: { labels: [], hasDescription: true, imageCount: 0 }, }, 3600, ); let completions = 0; const out = await resolveOrganizeMetadata( h.rt, release.id, { search: async () => [], fetch: async () => { throw new Error("unexpected fetch"); }, complete: async () => { completions++; return { text: "{}", model: "fixture-agent", inputTokens: 100, outputTokens: 5 }; }, }, { actor: { type: "system", id: "system:organize" } }, ); expect(completions).toBe(1); expect(out.mode).toBe("tmdb"); const identity = await h.db .selectFrom("external_identity") .selectAll() .where("media_entity_id", "=", out.mediaEntityId!) .where("provider", "=", "tmdb") .executeTakeFirstOrThrow(); expect(identity.external_id).toBe("990087"); const enrichment = await h.db .selectFrom("metadata_enrichment") .selectAll() .where("id", "=", out.enrichmentId!) .executeTakeFirstOrThrow(); expect((enrichment.usage as { inputTokens: number }).inputTokens).toBe(100); } finally { await h.close(); await tmdb.close(); } }); });