fix(organize-wizard): 整理向导只发用户改过的季集,不再绕过集数映射
- 行上新增 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
This commit is contained in:
1 parent
00045a24cb
commit
c16f8a51e9
6 files changed
+259
-17
No files matched your search
@@ -0,0 +1,110 @@
|
||||
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 }]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user