Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
122 lines
5.2 KiB
TypeScript
122 lines
5.2 KiB
TypeScript
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(/电影不用指定季集/)]);
|
|
});
|
|
});
|