import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { executeRecord } from "../../src/modules/acquisition-organize/services/organize.ts"; import { createFmHarness, type FmHarness, key, pgAvailable, seedEntity } from "./harness.ts"; const skip = !(await pgAvailable()); describe.skipIf(skip)("files 整理人工集号校验", () => { let h: FmHarness; let show: string; let film: string; const path = "Some.Show.S01E02.mkv"; const bytes = "x".repeat(64); beforeAll(async () => { h = await createFmHarness("overrides"); show = await seedEntity(h.db, { kind: "series", title: "Some Show", year: 2022 }); film = await seedEntity(h.db, { kind: "movie", title: "Film", year: 2021 }); await writeFile(join(h.dl, path), bytes); await mkdir(join(h.dl, "Season")); await writeFile(join(h.dl, "Season", path), bytes); }); afterAll(async () => h?.close()); const facts = async () => ({ entities: await h.db.selectFrom("media_entity").select("id").orderBy("id").execute(), identities: await h.db.selectFrom("external_identity").select("id").orderBy("id").execute(), plans: await h.db.selectFrom("artifact").select("id").where("title", "=", "files_plan").orderBy("id").execute(), records: await h.db.selectFrom("organization_record").select("id").orderBy("id").execute(), tasks: await h.db.selectFrom("task").select("id").orderBy("id").execute(), jobs: h.engine.enqueued.length, library: await readdir(h.lib, { recursive: true }), }); it.each([true, false])("非连续集号在 dryRun=%s 时整批拒绝,不创建作品、计划、记录或文件", async (dryRun) => { const before = await facts(); const r = await h.api("POST", "/files/organize", { idempotencyKey: key(), action: "copy", dryRun, items: [ // 排在坏输入前的远端候选也不能创建作品或发起 TMDB 请求。 { root: h.dlRoot, path, external: { provider: "tmdb", externalId: "136315", kind: "tv" } }, { root: h.dlRoot, path, mediaEntityId: show, season: 2, episodes: [5, 7] }, ], }); expect(r.status).toBe(400); expect(r.body.error.message).toMatch(/bad_request:.*集号 5,7 不连续/); expect(await facts()).toEqual(before); expect(await readFile(join(h.dl, path), "utf8")).toBe(bytes); }); it("远端剧集候选的不连续集号也在建档前拒绝", async () => { const before = await facts(); const r = await h.api("POST", "/files/organize", { idempotencyKey: key(), action: "copy", dryRun: true, items: [ { root: h.dlRoot, path, external: { provider: "tmdb", externalId: "136315", kind: "tv" }, episodes: [2, 4] }, ], }); expect(r.status).toBe(400); expect(r.body.error.message).toContain("不连续"); expect(await facts()).toEqual(before); }); it.each(["local", "external"])("%s 剧集目录指定集号时明确拒绝,不保存计划", async (source) => { const before = await facts(); const entity = source === "local" ? { mediaEntityId: show } : { external: { provider: "tmdb", externalId: "136315", kind: "tv" } }; const r = await h.api("POST", "/files/organize", { idempotencyKey: key(), action: "copy", dryRun: true, items: [{ root: h.dlRoot, path: "Season", ...entity, season: 1, episodes: [2] }], }); expect(r.status).toBe(400); expect(r.body.error.message).toContain("目录不能指定集号"); expect(await facts()).toEqual(before); }); it("连续人工季集覆盖原解析,预览与执行路径一致", async () => { const items = [{ root: h.dlRoot, path, mediaEntityId: show, season: 2, episodes: [4, 3, 4] }]; const dry = await h.api("POST", "/files/organize", { idempotencyKey: key(), action: "copy", dryRun: true, items }); expect(dry.status).toBe(200); const group = dry.body.data.groups[0]; expect(group.error).toBeNull(); expect(group.warnings).toEqual([]); expect(group.files).toHaveLength(1); expect(group.files[0].target).toMatch(/Season 2\/Some Show - S02E03-E04 - 第 3-4 集\.mkv$/); const exec = await h.api("POST", "/files/organize", { idempotencyKey: key(), action: "copy", dryRun: false, items, planIds: [group.planId], }); expect(exec.status).toBe(200); expect(exec.body.data.groups[0].error).toBeNull(); expect(exec.body.data.groups[0].files).toEqual(group.files); expect(exec.body.data.recordIds).toHaveLength(1); const result = await executeRecord(h.module.deps().ao, exec.body.data.recordIds[0]); expect(result.status).toBe("succeeded"); expect(await readFile(group.files[0].target, "utf8")).toBe(bytes); }); it("电影仍忽略季集覆写并提示", async () => { const r = await h.api("POST", "/files/organize", { idempotencyKey: key(), action: "copy", dryRun: true, items: [{ root: h.dlRoot, path, mediaEntityId: film, season: 1, episodes: [2, 4] }], }); expect(r.status).toBe(200); expect(r.body.data.groups[0].warnings).toEqual([expect.stringMatching(/电影不用指定季集/)]); }); });