Files
UHD_Media_Tool/apps/server/test/acquisition-organize/scrape-clearlogo.test.ts
T
rootandClaude Opus 5.5 680419bb64 feat(scrape): TMDB 标志写 clearlogo.png
- 富详情请求加 images(include_image_language=zh,en,null),作品原语言不是中英文时另取 /images;
  选一张 PNG 标志(原语言 → 中文 → 英文 → 无文字,同语言按评分)存作品字段 logoPath,变化时触发重刮
- 刮削布局新角色 clearlogo:电影写影片目录 clearlogo.png、剧集写剧集根目录,库根 / 平铺电影 <视频名>-clearlogo.png;
  覆盖方式与 hash 规则同其它图片
- 布局金样差分登记偏离 tmdb_clearlogo;mock TMDB 支持 logos 与 /images

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 19:37:34 +08:00

127 lines
5.3 KiB
TypeScript

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();
});
});