copy(server): 刮削原因与登录提示补改;同步数据清理、整理守卫等断言

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-07 02:05:14 +08:00
1 parent 0019dc1afe
commit c7a7d7ec2c
8 files changed
+14 -14

No files matched your search

@@ -355,7 +355,7 @@ export function organizeDecisionHook(d: AoDeps): DecisionHook {
`decision-${key}`, `decision-${key}`,
); );
if (r.status !== "running" && r.status !== "queued") if (r.status !== "running" && r.status !== "queued")
throw new HookError("conflict", `重新整理仍未能执行:${r.reason ?? r.status}`); throw new HookError("conflict", `重新整理没能执行:${r.reason ?? r.status}`);
return { kind: "organize", choice: key, ...r }; return { kind: "organize", choice: key, ...r };
}, },
}; };
@@ -494,7 +494,7 @@ export async function refreshSiteAccount(
if (r.kind === "ok") return await succeed(r.info); if (r.kind === "ok") return await succeed(r.info);
return await succeed( return await succeed(
null, null,
`登录成功;用户数据未取到:${r.kind === "no_parser" ? "站点没有用户数据解析器" : r.message}`, `登录成功,但没取到用户数据:${r.kind === "no_parser" ? "站点没有用户数据解析器" : r.message}`,
); );
} }
const plain = await secrets.resolve(acc.secret_ref); const plain = await secrets.resolve(acc.secret_ref);
@@ -240,7 +240,7 @@ describe.skipIf(!hasPg)("手动整理守卫:路径 / Move 授权 / 做种义务"
// 按下载任务 ID(管理员也一样被做种义务拦下) // 按下载任务 ID(管理员也一样被做种义务拦下)
const byId = await post({ downloadTaskId: dtId, action: "move" }); const byId = await post({ downloadTaskId: dtId, action: "move" });
expect(byId.status).toBe(409); expect(byId.status).toBe(409);
expect(byId.body.error.message).toContain("未改为复制"); expect(byId.body.error.message).toContain("没有改成复制");
// 按路径命中纳管任务的内容 // 按路径命中纳管任务的内容
const content = join(cache.root, "Seeding.2023.1080p.BluRay-HR.mkv"); const content = join(cache.root, "Seeding.2023.1080p.BluRay-HR.mkv");
expect(existsSync(content)).toBe(true); expect(existsSync(content)).toBe(true);
@@ -245,7 +245,7 @@ describe.skipIf(skip)("files W5-A2", () => {
expect(tvTargets.some((t: string | null) => t?.endsWith("Season 2/剧 - S02E03-E04 - 第 3-4 集.mkv"))).toBe(true); expect(tvTargets.some((t: string | null) => t?.endsWith("Season 2/剧 - S02E03-E04 - 第 3-4 集.mkv"))).toBe(true);
expect(tvTargets.some((t: string | null) => t?.endsWith("Season 2/剧 - S02E05-E06 - 第 5-6 集.mkv"))).toBe(true); expect(tvTargets.some((t: string | null) => t?.endsWith("Season 2/剧 - S02E05-E06 - 第 5-6 集.mkv"))).toBe(true);
expect(tv.warnings).toEqual([]); expect(tv.warnings).toEqual([]);
expect(mv.warnings).toEqual([expect.stringMatching(/电影不适用季\/集覆写/)]); expect(mv.warnings).toEqual([expect.stringMatching(/电影不用指定季集/)]);
expect(mv).toMatchObject({ action: "copy", slotId: slot.id, targetDir: join(h.lib, "picked") }); expect(mv).toMatchObject({ action: "copy", slotId: slot.id, targetDir: join(h.lib, "picked") });
expect(mv.files[0].target).toBe(join(h.lib, "picked", "Film (2019) - 1080p.mkv")); expect(mv.files[0].target).toBe(join(h.lib, "picked", "Film (2019) - 1080p.mkv"));
@@ -79,7 +79,7 @@ describe.skipIf(skip)("files 整理人工集号校验", () => {
items: [{ root: h.dlRoot, path: "Season", ...entity, season: 1, episodes: [2] }], items: [{ root: h.dlRoot, path: "Season", ...entity, season: 1, episodes: [2] }],
}); });
expect(r.status).toBe(400); expect(r.status).toBe(400);
expect(r.body.error.message).toContain("目录项不能指定集号"); expect(r.body.error.message).toContain("目录不能指定集号");
expect(await facts()).toEqual(before); expect(await facts()).toEqual(before);
}); });
@@ -116,6 +116,6 @@ describe.skipIf(skip)("files 整理人工集号校验", () => {
items: [{ root: h.dlRoot, path, mediaEntityId: film, season: 1, episodes: [2, 4] }], items: [{ root: h.dlRoot, path, mediaEntityId: film, season: 1, episodes: [2, 4] }],
}); });
expect(r.status).toBe(200); expect(r.status).toBe(200);
expect(r.body.data.groups[0].warnings).toEqual([expect.stringMatching(/电影不适用季\/集覆写/)]); expect(r.body.data.groups[0].warnings).toEqual([expect.stringMatching(/电影不用指定季集/)]);
}); });
}); });
+4 -4
View File
@@ -189,11 +189,11 @@ describe.skipIf(!hasPg)("数据库清理(真实 PG)", () => {
expect(Object.fromEntries(preview.map((p) => [p.label, p.rows]))).toEqual({ expect(Object.fromEntries(preview.map((p) => [p.label, p.rows]))).toEqual({
已投递事件: 3, 已投递事件: 3,
识别尝试: 2, 识别尝试: 2,
已识别的待识别条目: 1, 已识别的未识别资源: 1,
站点快照: 3, 站点抓取记录: 3,
}); });
// 站点快照 3 行、batchSize 2:分两批 // 站点快照 3 行、batchSize 2:分两批
expect(await run("pipeline")).toBe("已投递事件 3、识别尝试 2、已识别的待识别条目 1、站点快照 3 行已删除"); expect(await run("pipeline")).toBe("已投递事件 3、识别尝试 2、已识别的未识别资源 1、站点抓取记录 3 行已删除");
const types = await sql<{ id: string }>`select type as id from outbox_event`.execute(h.db); const types = await sql<{ id: string }>`select type as id from outbox_event`.execute(h.db);
expect(types.rows.map((r) => r.id).sort()).toEqual( expect(types.rows.map((r) => r.id).sort()).toEqual(
@@ -225,7 +225,7 @@ describe.skipIf(!hasPg)("数据库清理(真实 PG)", () => {
h.db, h.db,
); );
const counts = await countRules(h.db, job("pipeline").rules!(ctx())); const counts = await countRules(h.db, job("pipeline").rules!(ctx()));
expect(counts.find((p) => p.label === "站点快照")?.rows).toBe(1); expect(counts.find((p) => p.label === "站点抓取记录")?.rows).toBe(1);
}); });
it("定时任务登记:流水数据清理每天低峰一次(cron)", () => { it("定时任务登记:流水数据清理每天低峰一次(cron)", () => {
+1 -1
View File
@@ -251,7 +251,7 @@ describe.skipIf(!hasPg)("CACHE-2 存储(真实 PG)", () => {
await cand("r1", "unmatched", 31); await cand("r1", "unmatched", 31);
await cand("r2", "unmatched", 29); await cand("r2", "unmatched", 29);
await cand("r3", "ambiguous", 31); await cand("r3", "ambiguous", 31);
expect(await job("identity_pool")(now)).toBe("归档 1 条未识别条目"); expect(await job("identity_pool")(now)).toBe("归档 1 条未识别资源");
const st = await sql<{ t: string; s: string }>`select sr.title as t, c.status as s from identity_candidate c const st = await sql<{ t: string; s: string }>`select sr.title as t, c.status as s from identity_candidate c
join source_release sr on sr.id = c.source_release_id order by sr.title`.execute(h.db); join source_release sr on sr.id = c.source_release_id order by sr.title`.execute(h.db);
expect(st.rows).toEqual([ expect(st.rows).toEqual([
+3 -3
View File
@@ -102,7 +102,7 @@ const REASONS: Record<string, ReasonText> = {
// SCR-1 / SCR-2 刮削(刮削记录、逐文件结果) // SCR-1 / SCR-2 刮削(刮削记录、逐文件结果)
scrape_no_files: { text: "作品没有已整理文件", action: "到文件管理里对目录刮削" }, scrape_no_files: { text: "作品没有已整理文件", action: "到文件管理里对目录刮削" },
scrape_no_metadata: { text: "没有可用的元数据", action: "指定 TMDB 条目后重新刮削" }, scrape_no_metadata: { text: "没有可用的元数据", action: "指定 TMDB 条目后重新刮削" },
scrape_root_read_only: { text: "只登记模式的媒体库目录,不写文件" }, scrape_root_read_only: { text: "媒体库目录是只登记模式,不写文件" },
scrape_root_offline: { text: "目录不可访问或未挂载" }, scrape_root_offline: { text: "目录不可访问或未挂载" },
scrape_not_video: { text: "不是视频文件或目录" }, scrape_not_video: { text: "不是视频文件或目录" },
scrape_path_missing: { text: "目录或文件已不存在" }, scrape_path_missing: { text: "目录或文件已不存在" },
@@ -110,9 +110,9 @@ const REASONS: Record<string, ReasonText> = {
task_enqueue_failed: { text: "后台任务没排上", action: "稍后重试" }, task_enqueue_failed: { text: "后台任务没排上", action: "稍后重试" },
scrape_no_videos: { text: "目录里没有视频", action: "对作品或季目录刮削" }, scrape_no_videos: { text: "目录里没有视频", action: "对作品或季目录刮削" },
scrape_tmdb_unavailable: { text: "TMDB 不可用", action: "检查 TMDB 设置后重新刮削" }, scrape_tmdb_unavailable: { text: "TMDB 不可用", action: "检查 TMDB 设置后重新刮削" },
scrape_exists: { text: "文件已存在,不是本工具写的或只补缺失时不改" }, scrape_exists: { text: "已有文件,没有覆盖(不是本工具写的,或选了只补缺失)" },
scrape_unchanged: { text: "内容没有变化" }, scrape_unchanged: { text: "内容没有变化" },
scrape_user_modified: { text: "文件写入后被改过,更新时不覆盖", action: "要替换就选全部覆盖重新刮削" }, scrape_user_modified: { text: "文件写入后被改过,更新时不覆盖", action: "要替换就用「全部覆盖」重新刮削" },
scrape_metadata_missing: { text: "缺少这一季或这一集的元数据,没有写占位文件", action: "刷新元数据后重新刮削" }, scrape_metadata_missing: { text: "缺少这一季或这一集的元数据,没有写占位文件", action: "刷新元数据后重新刮削" },
scrape_internal_error: { text: "刮削过程中出错", action: "查看日志后重新刮削" }, scrape_internal_error: { text: "刮削过程中出错", action: "查看日志后重新刮削" },
scrape_write_failed: { text: "写入目录失败", action: "检查目录权限后重新刮削" }, scrape_write_failed: { text: "写入目录失败", action: "检查目录权限后重新刮削" },