feat(scrape): 任意目录就地刮削,按路径识别作品,每次刮削一条记录(SCR-2)
- POST /files/scrape 改为对目录 / 视频文件刮削:已登记用作品,否则按路径识别(本地 + TMDB), 认不准返回候选,选定后再提交;未登记直接用 TMDB 字段,addToLibrary 顺手建作品 - 作品详情重新刮削、入库后自动刮削改为对作品目录刮削,同一实现 - 迁移 0058_scrape_record 替代 media_scrape;GET /files/scrape-records - 与 MP ScrapingChain 的文件布局差分(7 个场景,3 处登记偏离) Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f60254e265
commit
87bf33b42b
29 files changed
+2272
-653
No files matched your search
@@ -5,6 +5,7 @@ import { DownloadStatus } from "./execution.ts";
|
||||
import { InstanceHealth, LibraryMediaKind } from "./instances.ts";
|
||||
import { OrganizationStatus, PublicOrganizationRecord } from "./organization.ts";
|
||||
import { SlotStatus } from "./policy.ts";
|
||||
import { ScrapeMode, ScrapeRecord } from "./scrape.ts";
|
||||
|
||||
// 文件管理(`/files`,docs/plan/06-file-manager-spec.md)。
|
||||
// 只能浏览已登记的库根与下载根;路径一律用 {root, path}:root 为 FileRootId,path 为根内相对路径("" = 根)。
|
||||
@@ -680,30 +681,59 @@ export const IdentifyCompletedPayload = z.object({
|
||||
});
|
||||
export type IdentifyCompletedPayload = z.infer<typeof IdentifyCompletedPayload>;
|
||||
|
||||
// 刮削(仅已登记作品)
|
||||
// 刮削(SCR-2,照 MP):任意目录 / 视频文件就地写 NFO 与图片;按路径识别作品(已登记直接用,未登记走 TMDB 匹配),
|
||||
// 认不准时返回候选(needsChoice),选定后带 mediaEntityId / external 再提交一次。
|
||||
|
||||
export const FilesScrapeItem = FileRef.extend({
|
||||
/** 指定本地作品(选定候选后回传);与 external 二选一 */
|
||||
mediaEntityId: Id.optional(),
|
||||
/** 指定 TMDB 条目(候选里只有 TMDB 的);不登记也能刮削 */
|
||||
external: FilesExternalCandidate.optional(),
|
||||
});
|
||||
export type FilesScrapeItem = z.infer<typeof FilesScrapeItem>;
|
||||
|
||||
export const FilesScrapeRequest = z.object({
|
||||
idempotencyKey: IdempotencyKey,
|
||||
mediaEntityId: Id,
|
||||
/** SCR-1:true 时另排 scrapeMedia,把 NFO(与 images 时的图片)写进媒体库目录 */
|
||||
writeNfo: z.boolean().default(false),
|
||||
images: z.boolean().default(false),
|
||||
items: z.array(FilesScrapeItem).min(1).max(100),
|
||||
/** 缺省按设置 organize.scrapeMode */
|
||||
mode: ScrapeMode.optional(),
|
||||
/** 指定的 TMDB 条目顺手加入媒体库(建作品);不影响刮削本身 */
|
||||
addToLibrary: z.boolean().default(false),
|
||||
});
|
||||
export type FilesScrapeRequest = z.infer<typeof FilesScrapeRequest>;
|
||||
|
||||
export const FilesScrapeNeedsChoice = z.object({
|
||||
root: FileRootId,
|
||||
path: z.string(),
|
||||
/** 从路径解析出的标题 */
|
||||
title: z.string().nullable(),
|
||||
/** 前 5 个候选(可能为空:让用户搜索) */
|
||||
candidates: z.array(FileIdentifyCandidate).max(5),
|
||||
warnings: z.array(z.string()),
|
||||
});
|
||||
export type FilesScrapeNeedsChoice = z.infer<typeof FilesScrapeNeedsChoice>;
|
||||
|
||||
export const FilesScrapeResult = z.object({
|
||||
mediaEntityId: Id,
|
||||
/** refreshMetadata 引擎 run id(异步;字段级刷新,用户锁优先,不动路径) */
|
||||
taskId: z.string(),
|
||||
/** 当前被用户锁定、刷新不会改动的字段 */
|
||||
lockedFields: z.array(z.string()),
|
||||
/** NFO/图片投影(SCR-1):queued = 已排 scrapeMedia,结果看作品刮削状态;not_implemented 已不再返回 */
|
||||
nfo: z.enum(["written", "skipped", "queued", "not_requested", "not_implemented"]),
|
||||
images: z.enum(["written", "skipped", "queued", "not_requested", "not_implemented"]),
|
||||
/** scrapeMedia 引擎 run id(writeNfo / images 时) */
|
||||
scrapeTaskId: z.string().nullable().optional(),
|
||||
/** 已提交的刮削(每项一条记录);pending=true 时仍有排队 / 进行中的,按 id 查 /files/scrape-records */
|
||||
records: z.array(ScrapeRecord),
|
||||
/** 认不准作品的项:选一个候选后带 mediaEntityId / external 重新提交 */
|
||||
needsChoice: z.array(FilesScrapeNeedsChoice),
|
||||
/** 没提交的项:scrape_root_read_only(只登记目录)/ scrape_not_video(不是视频或目录) */
|
||||
skipped: z.array(z.object({ root: FileRootId, path: z.string(), reason: z.string() })),
|
||||
pending: z.boolean(),
|
||||
/** addToLibrary 时加入媒体库的作品 */
|
||||
entities: z.array(z.object({ mediaEntityId: Id, title: z.string(), created: z.boolean() })),
|
||||
});
|
||||
export type FilesScrapeResult = z.infer<typeof FilesScrapeResult>;
|
||||
|
||||
export const FilesScrapeRecordsQuery = z.object({
|
||||
/** 逗号分隔的刮削记录 id(≤100) */
|
||||
ids: z.string().min(1).max(4000),
|
||||
});
|
||||
export type FilesScrapeRecordsQuery = z.infer<typeof FilesScrapeRecordsQuery>;
|
||||
export const FilesScrapeRecordsResult = z.object({ records: z.array(ScrapeRecord) });
|
||||
export type FilesScrapeRecordsResult = z.infer<typeof FilesScrapeRecordsResult>;
|
||||
|
||||
// 探测
|
||||
|
||||
export const FilesProbeRequest = z.object({ items: z.array(FileRef).min(1).max(20) });
|
||||
@@ -843,20 +873,18 @@ export const FilesEntityFromCandidateResult = z.object({
|
||||
});
|
||||
export type FilesEntityFromCandidateResult = z.infer<typeof FilesEntityFromCandidateResult>;
|
||||
|
||||
/** G5:批量刷新元数据(刮削):按作品去重;items 里的文件/目录归到其已登记资产的作品 */
|
||||
/** G5:批量刷新元数据:按作品去重;items 里的文件/目录归到其已登记资产的作品(刮削见 /files/scrape) */
|
||||
export const FilesScrapeBatchRequest = z.object({
|
||||
idempotencyKey: IdempotencyKey,
|
||||
items: z.array(FileRef).max(500).optional(),
|
||||
mediaEntityIds: z.array(Id).max(200).optional(),
|
||||
/** SCR-1:true = 刮削(排 scrapeMedia 写 NFO 与图片,覆盖方式按设置);false(缺省)= 刷新元数据 */
|
||||
write: z.boolean().default(false),
|
||||
});
|
||||
export type FilesScrapeBatchRequest = z.infer<typeof FilesScrapeBatchRequest>;
|
||||
export const FilesScrapeBatchResult = z.object({
|
||||
submitted: z.array(
|
||||
z.object({ mediaEntityId: Id, title: z.string(), taskId: z.string(), lockedFields: z.array(z.string()) }),
|
||||
),
|
||||
/** 未提交:not_registered(作品没有已登记资产)/ not_found / no_entity(文件不属于任何已登记作品) */
|
||||
/** 未提交:not_found / no_entity(文件不属于任何已登记作品) */
|
||||
skipped: z.array(z.object({ ref: z.string(), reason: z.string() })),
|
||||
});
|
||||
export type FilesScrapeBatchResult = z.infer<typeof FilesScrapeBatchResult>;
|
||||
|
||||
Reference in new issue
Block a user