import { readFile, writeFile } from "node:fs/promises"; import type { ScrapeFileResult, ScrapeMatch } from "@umt/contracts"; import { createTempTree, type TempTree } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { queueScrape, runScrapeRecord } from "../../src/modules/acquisition-organize/services/scrape.ts"; import { createHarness, type Harness, pgAvailable, seedBase } from "./harness.ts"; // TMDB 标志 → clearlogo.png:电影写影片目录、剧集写剧集根目录;覆盖方式与其它图片一致 // (更新:没变不重写、本工具写过且没被改过的按新图覆盖、被人改过的跳过;只补缺失;全部覆盖)。 const hasPg = await pgAvailable(); const at = new Date().toISOString(); const f = (value: unknown) => ({ value, source: "tmdb", language: null, at, locked: false }); const enc = (s: string) => new TextEncoder().encode(s); describe.skipIf(!hasPg)("刮削:TMDB 标志写 clearlogo.png", () => { let h: Harness; let lib: TempTree; let rootId = ""; beforeAll(async () => { lib = await createTempTree( { "Movies/星际穿越 (2014)/Interstellar.2014.mkv": 100, "TV/人生切割术 (2022)/Season 1/Severance.S01E01.mkv": 100, }, { sentinel: true, prefix: "umt-scrlogo-" }, ); h = await createHarness("scrape_logo", { // 内容 = 路径,方便看出写的是哪张 scrapeImage: async (p) => ({ content: enc(p), mime: p.endsWith(".png") ? "image/png" : "image/jpeg" }), }); rootId = (await seedBase(h.db, { libraryPath: lib.root, cachePath: lib.root, qbUrl: "http://127.0.0.1:9" })).rootId; }, 60_000); afterAll(async () => { await h?.close(); await lib?.cleanup(); }); async function entity(kind: "movie" | "series", title: string, meta: object) { return ( await h.db .insertInto("media_entity") .values({ kind, title, year: 2020, status: "active", metadata: JSON.stringify(meta) }) .returning("id") .executeTakeFirstOrThrow() ).id; } const setMeta = (id: string, meta: object) => h.db .updateTable("media_entity") .set({ metadata: JSON.stringify(meta) }) .where("id", "=", id) .execute(); async function scrape( entityId: string, kind: "movie" | "tv", path: string, mode?: "missing" | "overwrite" | "update", ) { const match: ScrapeMatch = { mediaEntityId: entityId, provider: null, externalId: null, kind, title: "x", year: null, source: "library", }; const q = await queueScrape(h.d, { rootId: `lib:${rootId}`, rootPath: lib.root, libraryRootId: rootId, path, match, mode, trigger: "manual", }); return runScrapeRecord(h.d, q.recordId); } const logo = (files: ScrapeFileResult[]) => files.find((x) => x.role === "clearlogo"); const MOVIE_DIR = "Movies/星际穿越 (2014)"; const LOGO = `${MOVIE_DIR}/clearlogo.png`; it("电影:写在影片目录 clearlogo.png;更新模式内容没变不重写,换了标志按新图覆盖", async () => { const movie = await entity("movie", "星际穿越", { title: f("星际穿越"), logoPath: f("/logo-a.png") }); const r = await scrape(movie, "movie", MOVIE_DIR); expect(logo(r.files)).toMatchObject({ path: LOGO, action: "written", hash: expect.stringMatching(/^[0-9a-f]{64}$/), }); expect(await readFile(lib.path(LOGO), "utf8")).toBe("/logo-a.png"); const same = await scrape(movie, "movie", MOVIE_DIR, "update"); expect(logo(same.files)).toMatchObject({ action: "skipped", reason: "scrape_unchanged" }); await setMeta(movie, { title: f("星际穿越"), logoPath: f("/logo-b.png") }); const upd = await scrape(movie, "movie", MOVIE_DIR, "update"); expect(logo(upd.files)).toMatchObject({ action: "written" }); expect(await readFile(lib.path(LOGO), "utf8")).toBe("/logo-b.png"); // 被人换掉的标志:更新跳过、只补缺失跳过、全部覆盖改写 await writeFile(lib.path(LOGO), "我自己的标志"); await setMeta(movie, { title: f("星际穿越"), logoPath: f("/logo-c.png") }); expect(logo((await scrape(movie, "movie", MOVIE_DIR, "update")).files)).toMatchObject({ action: "skipped", reason: "scrape_user_modified", }); expect(logo((await scrape(movie, "movie", MOVIE_DIR, "missing")).files)).toMatchObject({ action: "skipped", reason: "scrape_exists", }); expect(await readFile(lib.path(LOGO), "utf8")).toBe("我自己的标志"); expect(logo((await scrape(movie, "movie", MOVIE_DIR, "overwrite")).files)).toMatchObject({ action: "written" }); expect(await readFile(lib.path(LOGO), "utf8")).toBe("/logo-c.png"); }); it("剧集:写在剧集根目录,季目录不写;没有标志不写", async () => { const show = await entity("series", "人生切割术", { title: f("人生切割术"), logoPath: f("/sev.png") }); const r = await scrape(show, "tv", "TV/人生切割术 (2022)"); expect(logo(r.files)).toMatchObject({ path: "TV/人生切割术 (2022)/clearlogo.png", action: "written" }); expect(r.files.filter((x) => x.role === "clearlogo")).toHaveLength(1); await setMeta(show, { title: f("人生切割术") }); const none = await scrape(show, "tv", "TV/人生切割术 (2022)", "overwrite"); expect(logo(none.files)).toBeUndefined(); }); });