- 行上新增 episodeTouched / mappedFor:只有行内改季集、批量季号、集号偏移、弹窗指定季号的行才发季集; 未改动的预填只作展示,交给服务端按最终作品映射 - 改选作品后,按旧作品映射的预填清空(未映射的解析值保留);补识别刷新未改动行的预填 - 识别结果新增 episodeUnresolved(集数待确认),向导里该行未决,需填季集或排除 - pg 测试:向导整理 `[ANi] X - 25` 不带季集 → S02E13,显式季集按用户值 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
111 lines
4.3 KiB
TypeScript
111 lines
4.3 KiB
TypeScript
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<string, unknown>) => {
|
|
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 }]);
|
|
});
|
|
});
|