feat(scrape): 入库后自动刮削缺省开启,没排上时在整理记录上记「未刮削(原因)」

- scrape_record 状态加 skipped(迁移 0074);目标不在媒体库、库根只登记、作品没有 TMDB id 也没有本地元数据、排队失败时
  留一条挂在整理记录上的「未刮削」记录(同原因只记一次),设置关闭不记;
- 缺省开启 scrapeAfterImport,只对有 TMDB id 或本地元数据的作品排;
- 已结束记录重放与收尾同一口径补排刮削(按整理记录幂等);
- 整理详情「整理后刮削」显示未刮削原因,并列出失败与非预期跳过(缺元数据、认不出集号)的文件。

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-03 23:19:28 +08:00
1 parent 8c4c7d028a
commit cdf62dbf02
12 files changed
+320 -49

No files matched your search

+2
View File
@@ -117,6 +117,8 @@ const REASONS: Record<string, ReasonText> = {
scrape_image_failed: { text: "图片没下载成功", action: "稍后重新刮削" },
scrape_no_episode: { text: "文件名里认不出集号" },
scrape_no_work_dir: { text: "视频直接放在媒体库目录下,不写剧集和季的文件" },
scrape_canceled: { text: "刮削任务被取消", action: "重新刮削" },
scrape_not_library: { text: "整理目标不在媒体库目录里,不自动刮削", action: "到文件管理里对目录刮削" },
};
/**
+2 -1
View File
@@ -26,7 +26,8 @@ export type ScrapeFileResult = z.infer<typeof ScrapeFileResult>;
export const ScrapeMode = z.enum(["missing", "overwrite", "update"]);
export type ScrapeMode = z.infer<typeof ScrapeMode>;
export const ScrapeStatus = z.enum(["queued", "running", "succeeded", "partial", "failed"]);
/** skipped = 入库后自动刮削没排上(原因在 reason;0074),整理详情显示「未刮削」 */
export const ScrapeStatus = z.enum(["queued", "running", "succeeded", "partial", "failed", "skipped"]);
export type ScrapeStatus = z.infer<typeof ScrapeStatus>;
export const ScrapeTrigger = z.enum(["auto", "manual"]);
export type ScrapeTrigger = z.infer<typeof ScrapeTrigger>;
+2 -1
View File
@@ -644,7 +644,8 @@ export const DEFAULT_SETTINGS: GlobalSettings = {
defaultMode: "copy",
allowCrossVolumeMove: true,
minSizeBytes: 50 * 1024 * 1024,
scrapeAfterImport: false,
// 缺省开:只对有 TMDB id 或本地元数据的作品排刮削(否则在整理记录上记「未刮削」),「更新」方式不动别人写的文件
scrapeAfterImport: true,
scrapeMode: "update",
scrapeImages: true,
scrapeEpisodeThumbs: true,
@@ -0,0 +1,24 @@
import type { Kysely } from "kysely";
import { execDdl, oneOf } from "../src/migration-helpers.ts";
// 入库后自动刮削没排上(库根不是可写管理模式、作品没有 TMDB id 也没有本地元数据、排队失败)时在整理记录上可见:
// 写一条 status = skipped 的刮削记录(原因码在 reason),整理详情的「整理后刮削」里显示「未刮削(原因)」。
// 设置关闭属预期,不写记录。
const OLD = ["queued", "running", "succeeded", "partial", "failed"];
const NEW = [...OLD, "skipped"];
const swap = (values: readonly string[]) => `
alter table scrape_record drop constraint if exists scrape_record_status_check;
alter table scrape_record add constraint scrape_record_status_check ${oneOf("status", values)};
`;
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(db, swap(NEW));
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(db, `delete from scrape_record where status = 'skipped'; ${swap(OLD)}`);
}
+1 -1
View File
@@ -231,7 +231,7 @@ export type ScheduledJobKind = "task" | "agent_check";
export type ScheduledJobLastStatus = "ok" | "failed" | "skipped" | "running";
export type ScheduledJobRunTrigger = "schedule" | "manual" | "skip";
export type ScheduledJobRunStatus = "ok" | "failed" | "skipped" | "running";
export type ScrapeRecordStatus = "queued" | "running" | "succeeded" | "partial" | "failed";
export type ScrapeRecordStatus = "queued" | "running" | "succeeded" | "partial" | "failed" | "skipped";
export type ScrapeRecordTrigger = "auto" | "manual";
export type ScrapeRecordMode = "missing" | "overwrite" | "update";
export type SecretPurpose =