import { AUDIENCES_FIXTURE_RELEASES, createMockNexusPhpSite, createMockTmdb, type MockNexusPhpSite, type MockTmdb, } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { DETAIL_PAGE_REGION } from "../../src/modules/sources-identity/service/detail-page.ts"; import { resolveRelease } from "../../src/modules/sources-identity/service/resolve.ts"; import { snapshotSite } from "../../src/modules/sources-identity/service/snapshot.ts"; import { sharedCache } from "../../src/shared-cache.ts"; import { createHarness, type Harness, json, pgAvailable, seedSite } from "./harness.ts"; const up = await pgAvailable(); const SHOT = "http://93.184.216.34/shots/1.png"; const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4]); describe.skipIf(!up)("站点详情页扩展内容:按需抓 + 24 小时缓存 + 截图代理 + 外部 ID 进身份解析", () => { let h: Harness; let mock: MockNexusPhpSite; let tmdb: MockTmdb; let siteId: string; let relId: string; const imageFetches: string[] = []; beforeAll(async () => { const releases = AUDIENCES_FIXTURE_RELEASES.map((r) => r.id === "812345" ? { ...r, descriptionHtml: `◎片名 沙丘2
◎IMDb tt15239678
General
Complete name : Dune.mkv
Format : Matroska
File size : 78 GiB
Duration : 2 h 46 min
Video
Format : HEVC
`, } : r, ); mock = await createMockNexusPhpSite({ releases }); tmdb = await createMockTmdb(); h = await createHarness("detailpage", { tmdbUrl: tmdb.url, fetch: (input, init) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; if (url.startsWith("http://93.184.216.34/")) { imageFetches.push(url); return Promise.resolve(new Response(PNG, { status: 200, headers: { "content-type": "image/png" } })); } return fetch(input, init); }, }); siteId = (await seedSite(h, { baseUrl: `${mock.baseUrl}/`, cookie: mock.cookie })).site.id; await snapshotSite(h.rt, { siteId, kind: "manual" }); relId = ( await h.db .selectFrom("source_release") .select(["id"]) .where("site_release_id", "=", "812345") .executeTakeFirstOrThrow() ).id; }); afterAll(async () => { await h?.close(); await mock?.close(); await tmdb?.close(); }); const detailRequests = () => mock.requests.filter((r) => r.path.startsWith("/details.php")).length; it("未抓过:page 为 null,标明站点支持详情页;不刷新就不请求站点", async () => { const before = detailRequests(); const d = await json(await h.req(`/sites/${siteId}/releases/${relId}`)); expect(d.status).toBe(200); expect(d.body.data).toMatchObject({ page: null, detailSupported: true }); expect(detailRequests()).toBe(before); }); it("refresh 抓一次:简介 / MediaInfo / 截图(代理路径)/ IMDb;外部 ID 写 raw._detail;缓存期内不再请求", async () => { const before = detailRequests(); const d = await json(await h.req(`/sites/${siteId}/releases/${relId}?refresh=true`)); expect(d.body.data.refreshError).toBeNull(); expect(detailRequests()).toBe(before + 1); const page = d.body.data.page; expect(page).toMatchObject({ imdbId: "tt15239678", screenshots: [{ url: `/api/v1/img/site-detail/${relId}/0`, host: "93.184.216.34" }], }); expect(page.mediaInfo).toMatch(/^General\nComplete name : Dune.mkv/); expect(page.descriptionText).toContain("◎片名 沙丘2"); expect(page.descriptionText).not.toContain("Complete name"); // 展示结构:读缓存时现算 expect(page.infoBlock).toContainEqual({ key: "片名", value: "沙丘2" }); expect(page.media).toMatchObject({ kind: "mediainfo", general: { fileName: "Dune.mkv", container: "Matroska", size: "78 GiB", duration: "2 h 46 min" }, video: [{ codec: "HEVC" }], }); expect(Array.isArray(page.fileTree)).toBe(true); const row = await h.db .selectFrom("source_release") .select(["raw"]) .where("id", "=", relId) .executeTakeFirstOrThrow(); expect((row.raw as { _detail?: unknown })._detail).toEqual({ imdbId: "tt15239678" }); const again = await json(await h.req(`/sites/${siteId}/releases/${relId}?refresh=true`)); expect(again.body.data.page.imdbId).toBe("tt15239678"); expect(detailRequests()).toBe(before + 1); // 列表快照覆盖 raw._site 后,详情给出的外部 ID 仍在 await snapshotSite(h.rt, { siteId, kind: "manual" }); const row2 = await h.db .selectFrom("source_release") .select(["raw"]) .where("id", "=", relId) .executeTakeFirstOrThrow(); expect((row2.raw as { _detail?: unknown })._detail).toEqual({ imdbId: "tt15239678" }); }); it("截图代理:只取缓存里的第 index 张,按文件头认图片;越界 404", async () => { const res = await h.req(`/img/site-detail/${relId}/0`); expect(res.status).toBe(200); expect(res.headers.get("content-type")).toBe("image/png"); expect(new Uint8Array(await res.arrayBuffer())).toEqual(PNG); expect(imageFetches).toEqual([SHOT]); expect((await h.req(`/img/site-detail/${relId}/5`)).status).toBe(404); expect((await h.req(`/img/site-detail/not-an-id/0`)).status).toBe(400); }); it("缓存丢了(kv_cache 过期)再刷新会重新抓一次", async () => { await sharedCache(h.ctx).delete(DETAIL_PAGE_REGION, relId); const before = detailRequests(); const d = await json(await h.req(`/sites/${siteId}/releases/${relId}`)); expect(d.body.data.page).toBeNull(); await h.req(`/sites/${siteId}/releases/${relId}?refresh=true`); expect(detailRequests()).toBe(before + 1); }); it("身份解析:详情页的 IMDb ID 走 TMDB find,不按标题搜索,显式 ID 证据", async () => { const searches = () => tmdb.callsTo(/\/3\/search\//).length; const s0 = searches(); const res = await resolveRelease(h.rt, { sourceReleaseId: relId, force: true, trigger: "manual" }); expect(tmdb.callsTo(/\/3\/find\/tt15239678/).length).toBeGreaterThan(0); expect(searches()).toBe(s0); expect(res.outcome).toBe("auto"); const ev = await h.db .selectFrom("source_release") .select(["resolution_evidence", "media_entity_id"]) .where("id", "=", relId) .executeTakeFirstOrThrow(); expect(ev.media_entity_id).toBe(res.mediaEntityId); const ext = await h.db .selectFrom("external_identity") .select(["provider", "external_id"]) .where("media_entity_id", "=", res.mediaEntityId!) .where("provider", "=", "tmdb") .executeTakeFirstOrThrow(); expect(ext.external_id).toBe("693134"); }); });