import { 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, READ_KEY, seedEntity } from "./harness.ts"; // 整理向导不绕过集数映射:向导只发用户改过的季集;未带季集的项由服务端按最终作品映射(`- 25` → S02E13), // 带了季集的按人工指定执行。集数待确认的识别项带 episodeUnresolved,向导据此标为未决。 const skip = !(await pgAvailable()); describe.skipIf(skip)("整理向导执行时按作品映射集数", () => { let h: FmHarness; let show: string; const auto = "[ANi] Kusuriya no Hitorigoto - 25 [1080P].mkv"; const manual = "[ANi] Kusuriya no Hitorigoto - 25 [720P].mkv"; const pending = "[ANi] Kusuriya no Hitorigoto - 40 [1080P].mkv"; beforeAll(async () => { h = await createFmHarness("wizard_episode_mapping"); show = await seedEntity(h.db, { kind: "series", title: "Kusuriya no Hitorigoto", year: 2023 }); await h.db .updateTable("media_entity") .set({ metadata: JSON.stringify({ airStatus: { value: "Ended", source: "tmdb" } }) }) .where("id", "=", show) .execute(); for (const [season, total] of [ [1, 12], [2, 13], ] as const) { await h.db .insertInto("content_unit") .values({ media_entity_id: show, kind: "season", season_number: season, episode_total: total, status: "confirmed", source: "tmdb", }) .execute(); } await writeFile(join(h.dl, auto), "a".repeat(64)); await writeFile(join(h.dl, manual), "m".repeat(64)); await writeFile(join(h.dl, pending), "p".repeat(64)); }); afterAll(async () => h?.close()); const organize = async (item: Record) => { const items = [{ root: h.dlRoot, mediaEntityId: show, ...item }]; 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(); 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.recordIds).toHaveLength(1); const result = await executeRecord(h.module.deps().ao, exec.body.data.recordIds[0]); expect(result.status).toBe("succeeded"); return group.files[0] as { target: string }; }; const unitsOf = async (target: string) => h.db .selectFrom("asset_location as l") .innerJoin("asset_unit_coverage as au", "au.media_asset_id", "l.media_asset_id") .innerJoin("content_unit as u", "u.id", "au.content_unit_id") .select(["u.season_number", "u.episode_number"]) .where("l.relative_path", "like", `%${target.split("/").pop()}`) .execute(); it("识别:集数待确认的项带 episodeUnresolved,能映射的不带", async () => { const r = await h.api( "POST", "/files/identify", { items: [ { root: h.dlRoot, path: auto }, { root: h.dlRoot, path: pending }, ], minSizeBytes: 0, }, READ_KEY, ); expect(r.status).toBe(200); const [ok, bad] = r.body.data.items; expect(ok.parsed).toMatchObject({ season: 2, episodes: [13] }); expect(ok.episodeUnresolved).toBeUndefined(); expect(bad.episodeUnresolved).toBe(true); }); it("不改季集直接执行:`- 25` 按作品季表入库为 S02E13", async () => { const file = await organize({ path: auto }); expect(file.target).toMatch(/Season 2\/.*S02E13/); expect(await readFile(file.target, "utf8")).toBe("a".repeat(64)); expect(await unitsOf(file.target)).toEqual([{ season_number: 2, episode_number: 13 }]); }); it("用户显式改季集:按用户值入库", async () => { const file = await organize({ path: manual, season: 2, episodes: [12] }); expect(file.target).toMatch(/Season 2\/.*S02E12/); expect(await unitsOf(file.target)).toEqual([{ season_number: 2, episode_number: 12 }]); }); });