fix(organize): stop pending sidecars when replaying failed main files

This commit is contained in:
root committed 2026-10-13 15:38:51 +08:00
1 parent b306d27d08
commit 09a4c9fc1e
2 files changed
+48 -1

No files matched your search

@@ -166,7 +166,10 @@ export async function executeRecord(
.orderBy("ordinal")
.execute();
const planned = new Map(frozen.organize.files.map((f) => [f.src, f]));
const failedMains = new Set<string>();
// 主文件的失败可能已在上次执行提交;重放也要阻止随行文件脱离主文件继续搬运。
const failedMains = new Set(
files.filter((f) => f.role === "main" && f.status === "failed").map((f) => f.source_path),
);
/** 本轮 TIMEOUT/IO 类失败、留待任务重试的文件(结果不入账本,文件结果保持 running) */
const deferred: {
src: string;
@@ -50,6 +50,8 @@ describe.skipIf(!hasPg)("整理卡死回收(真实 PG + mock qB + 临时目录)"
"stall/Stall.Show.S01E01.1080p.WEB-DL.x264-GRP.mkv": SIZE,
"cap/Cap.Show.S01E01.1080p.WEB-DL.x264-GRP.mkv": SIZE,
"offline/Offline.Show.S01E01.1080p.WEB-DL.x264-GRP.mkv": SIZE,
"sidecar/Sidecar.Show.S01E01.1080p.WEB-DL.x264-GRP.mkv": SIZE,
"sidecar/Sidecar.Show.S01E01.1080p.WEB-DL.x264-GRP.srt": 100,
},
{ prefix: "umt-reccache-" },
);
@@ -208,6 +210,48 @@ describe.skipIf(!hasPg)("整理卡死回收(真实 PG + mock qB + 临时目录)"
expect((await recoverStalledRecords(h.d)).failed).toBe(0);
});
it.each(["copy", "move"] as const)("%s 重放已失败的主文件:未完成字幕留在源目录,不再搬运", async (action) => {
const entityId = await seedEntity(h.db, { kind: "series", title: `字幕恢复 ${action}`, year: 2024 });
const plan = await planImport(h.d, {
sourcePaths: [cache.path("sidecar")],
mediaEntityId: entityId,
action,
trigger: "manual",
actorType: "user",
});
expect(plan.status, JSON.stringify(plan)).toBe("running");
const recordId = plan.recordId!;
// 主文件失败已经提交,进程在处理随行字幕前退出;恢复时主文件不会再次执行。
await h.db
.updateTable("organization_file_result")
.set({ status: "failed", error: JSON.stringify({ code: "EEXIST", message: "目标冲突" }) })
.where("organization_record_id", "=", recordId)
.where("role", "=", "main")
.execute();
const sidecar = await h.db
.selectFrom("organization_file_result")
.selectAll()
.where("organization_record_id", "=", recordId)
.where("role", "=", "subtitle")
.executeTakeFirstOrThrow();
expect(sidecar.status).toBe("queued");
expect(sidecar.target_path).not.toBeNull();
expect((await executeRecord(h.d, recordId)).status).toBe("failed");
const result = await h.db
.selectFrom("organization_file_result")
.selectAll()
.where("id", "=", sidecar.id)
.executeTakeFirstOrThrow();
expect(result).toMatchObject({ status: "skipped", skip_reason: "main_failed", asset_operation_id: null });
expect(result.finished_at).not.toBeNull();
expect(existsSync(sidecar.source_path)).toBe(true);
expect(existsSync(join(lib.root, sidecar.target_path!))).toBe(false);
expect(
await h.db.selectFrom("asset_operation").select("id").where("organization_record_id", "=", recordId).execute(),
).toEqual([]);
});
it("库根离线:不抛错耗尽重试,记录等待并开待处理;恢复后巡检重投(不计上限)并完成", async () => {
const id = await manualRecord("offline", "离线的剧");
await rm(join(lib.root, ".uhd-root"));