feat(server): scrapeMedia 刮削任务,入库后自动刮削(SCR-1)
- 文件道任务 scrapeMedia:NFO 原子写入、图片经图片缓存下载(并发 4,失败不阻塞 NFO)、只补缺失 / 全部覆盖
- 设置 organize.scrapeAfterImport / scrapeMode / scrapeImages / scrapeEpisodeThumbs
- POST /library/{id}/scrape、详情 scrape 状态;files/scrape writeNfo 与 scrape-batch write 走同一任务
- 迁移 0057_media_scrape、原因码、审计 organize.scrape
- 与 MP 输出的 NFO 差分测试(apps/server/test/scrape)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
861f799615
commit
f38eca419e
24 files changed
+1740
-15
No files matched your search
@@ -370,6 +370,81 @@ export const LibraryDroppedPlanView = z.object({
|
||||
});
|
||||
export type LibraryDroppedPlanView = z.infer<typeof LibraryDroppedPlanView>;
|
||||
|
||||
/** SCR-1 刮削:一个写入目标(NFO / 图片)的结果;path 为库根内相对路径 */
|
||||
export const ScrapeFileResult = z.object({
|
||||
path: z.string(),
|
||||
kind: z.enum(["nfo", "image"]),
|
||||
/** movie_nfo / tvshow_nfo / season_nfo / episode_nfo / poster / fanart / season_poster / episode_thumb */
|
||||
role: z.string(),
|
||||
action: z.enum(["written", "skipped", "failed"]),
|
||||
/** 跳过 / 失败原因码(reasons.ts:scrape_exists / scrape_write_failed / scrape_image_failed …) */
|
||||
reason: z.string().nullable(),
|
||||
});
|
||||
export type ScrapeFileResult = z.infer<typeof ScrapeFileResult>;
|
||||
|
||||
export const ScrapeMode = z.enum(["missing", "overwrite"]);
|
||||
export type ScrapeMode = z.infer<typeof ScrapeMode>;
|
||||
|
||||
export const ScrapeStatus = z.enum(["queued", "running", "succeeded", "partial", "failed"]);
|
||||
export type ScrapeStatus = z.infer<typeof ScrapeStatus>;
|
||||
export const ScrapeTrigger = z.enum(["auto", "manual"]);
|
||||
export type ScrapeTrigger = z.infer<typeof ScrapeTrigger>;
|
||||
|
||||
/** 表 media_scrape 的行(一部作品一行,最近一次刮削) */
|
||||
export const MediaScrape = z.object({
|
||||
mediaEntityId: Id,
|
||||
status: ScrapeStatus,
|
||||
trigger: ScrapeTrigger,
|
||||
mode: ScrapeMode,
|
||||
recordId: Id.nullable(),
|
||||
engineRunId: z.string().nullable(),
|
||||
requestedAt: Timestamp,
|
||||
startedAt: Timestamp.nullable(),
|
||||
finishedAt: Timestamp.nullable(),
|
||||
written: NonNegInt,
|
||||
skipped: NonNegInt,
|
||||
failed: NonNegInt,
|
||||
reason: z.string().nullable(),
|
||||
files: z.array(ScrapeFileResult),
|
||||
createdAt: Timestamp,
|
||||
updatedAt: Timestamp,
|
||||
});
|
||||
export type MediaScrape = z.infer<typeof MediaScrape>;
|
||||
|
||||
/** SCR-1 作品刮削状态(最近一次;表 media_scrape) */
|
||||
export const LibraryScrapeState = z.object({
|
||||
status: ScrapeStatus,
|
||||
trigger: ScrapeTrigger,
|
||||
mode: ScrapeMode,
|
||||
requestedAt: Timestamp,
|
||||
startedAt: Timestamp.nullable(),
|
||||
finishedAt: Timestamp.nullable(),
|
||||
written: NonNegInt,
|
||||
skipped: NonNegInt,
|
||||
failed: NonNegInt,
|
||||
/** 整体失败原因码(没有文件、没有元数据等) */
|
||||
reason: z.string().nullable(),
|
||||
files: z.array(ScrapeFileResult),
|
||||
/** 触发它的整理记录(入库后自动刮削) */
|
||||
recordId: Id.nullable(),
|
||||
/** 引擎 run id */
|
||||
taskId: z.string().nullable(),
|
||||
});
|
||||
export type LibraryScrapeState = z.infer<typeof LibraryScrapeState>;
|
||||
|
||||
export const LibraryScrapeRequest = z.object({
|
||||
/** 缺省按设置 organize.scrapeMode */
|
||||
mode: ScrapeMode.optional(),
|
||||
});
|
||||
export type LibraryScrapeRequest = z.infer<typeof LibraryScrapeRequest>;
|
||||
export const LibraryScrapeResult = z.object({
|
||||
mediaEntityId: Id,
|
||||
taskId: z.string(),
|
||||
/** 同一作品已有排队中的刮削,直接复用 */
|
||||
deduplicated: z.boolean(),
|
||||
});
|
||||
export type LibraryScrapeResult = z.infer<typeof LibraryScrapeResult>;
|
||||
|
||||
export const LibraryDetail = z.object({
|
||||
entity: MediaEntity,
|
||||
/** 身份映射状态(含 proposed/conflicted,前端区分展示) */
|
||||
@@ -403,6 +478,8 @@ export const LibraryDetail = z.object({
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
/** SCR-1:最近一次刮削(写 NFO 与图片);没刮削过为 null */
|
||||
scrape: LibraryScrapeState.nullable().optional(),
|
||||
});
|
||||
export type LibraryDetail = z.infer<typeof LibraryDetail>;
|
||||
|
||||
|
||||
Reference in new issue
Block a user