diff --git a/apps/web/app/api/files.ts b/apps/web/app/api/files.ts index b0b4f5b5..0156807e 100644 --- a/apps/web/app/api/files.ts +++ b/apps/web/app/api/files.ts @@ -26,6 +26,7 @@ import { FilesRenameResult, type FilesScrapeBatchRequest, FilesScrapeBatchResult, + FilesScrapeRecordsResult, type FilesScrapeRequest, FilesScrapeResult, type FilesSuggestNameRequest, @@ -63,11 +64,14 @@ export type { FilesOrganizeItem, FilesOrganizeRequest, FilesScrapeBatchResult, + FilesScrapeNeedsChoice, + FilesScrapeResult, FilesSuggestNameResult, FilesTransferResult, FilesTreeResult, IdentifyCompletedPayload, NamingTemplateSource, + ScrapeRecord, TmdbCandidate, TrashItem, } from "@umt/contracts"; @@ -271,6 +275,19 @@ export function useFileRecords(q: { ids?: string[]; planIds?: string[] }, enable }); } +/** 刮削记录回查:排队 / 进行中时每 2 秒刷新 */ +export function useScrapeRecords(ids: string[]) { + return useQuery({ + queryKey: ["files", "scrape-records", ids.join(",")], + enabled: ids.length > 0, + retry: false, + queryFn: ({ signal }) => + call(FilesScrapeRecordsResult, "/files/scrape-records", { params: { ids: ids.join(",") }, signal }), + refetchInterval: (query) => + query.state.data?.records.some((r) => r.status === "queued" || r.status === "running") ? 2_000 : false, + }); +} + /** 整理记录 → 源 FileRef(整理记录页"重新整理") */ export function useRecordSources(recordId: string | null) { return useQuery({ @@ -372,8 +389,8 @@ export const useOrganizeFiles = () => useFilesMutation((b: FilesOrganizeRequest) => post(FilesOrganizeResult, "/files/organize", b), { invalidate: notDry, }); -export const useScrape = () => - useMutation({ mutationFn: (b: FilesScrapeRequest) => post(FilesScrapeResult, "/files/scrape", b) }); +/** 刮削(SCR-2):任意目录 / 视频文件就地写 NFO 与图片;认不准的项在 needsChoice 里,选定后再提交 */ +export const useScrape = () => useFilesMutation((b: FilesScrapeRequest) => post(FilesScrapeResult, "/files/scrape", b)); export const useProbe = () => useFilesMutation((b: FilesProbeRequest) => post(FilesProbeResult, "/files/probe", b)); export const useScrapeBatch = () => useMutation({ mutationFn: (b: FilesScrapeBatchRequest) => post(FilesScrapeBatchResult, "/files/scrape-batch", b) }); diff --git a/apps/web/app/api/library.ts b/apps/web/app/api/library.ts index c4899ffe..ad48ff74 100644 --- a/apps/web/app/api/library.ts +++ b/apps/web/app/api/library.ts @@ -82,7 +82,7 @@ export function useLibraryDetail(id: string | undefined) { }); } -/** SCR-1 刮削:写 NFO 与图片到媒体库目录(后台任务);mode 缺省按设置 */ +/** 刮削:对作品目录就地写 NFO 与图片(后台任务,SCR-2 与文件管理「刮削」同一实现);mode 缺省按设置 */ export function useScrapeEntity(id: string) { const qc = useQueryClient(); return useMutation({ diff --git a/apps/web/app/components/files/dialogs.tsx b/apps/web/app/components/files/dialogs.tsx index 51c492a1..9d026129 100644 --- a/apps/web/app/components/files/dialogs.tsx +++ b/apps/web/app/components/files/dialogs.tsx @@ -1,6 +1,7 @@ import { ArrowRight, Copy, + FileDown, FolderInput, FolderPlus, LoaderCircle, @@ -19,6 +20,7 @@ import { type FileRef, type FileRoot, type FilesScrapeBatchResult, + type FilesScrapeNeedsChoice, type FilesSuggestNameResult, type FilesTransferResult, filesErrorReason, @@ -28,11 +30,17 @@ import { useMkdir, useMove, useRename, + useScrape, useScrapeBatch, + useScrapeRecords, useSuggestName, } from "~/api/files.ts"; +import { useSettings } from "~/api/settings.ts"; import { FormField } from "~/components/console/form-field.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; +import { ScrapeRecordView } from "~/components/library/scrape-panel.tsx"; +import { EntityPicker } from "~/components/organize-wizard/entity-picker.tsx"; +import type { WizardEntity } from "~/components/organize-wizard/model.ts"; import { PathSegments } from "~/components/organize-wizard/path-segments.tsx"; import { Button } from "~/components/ui/button.tsx"; import { @@ -986,21 +994,12 @@ export function CopyPathDialog({ root, entry, onClose }: { root: FileRoot; entry ); } -// ─── 刷新元数据 / 刮削 ───────────────────────────────────── +// ─── 刷新元数据 ───────────────────────────────────────────── /** - * 刷新元数据(§7.2):只对已登记项;多选时按作品去重(POST /files/scrape-batch)。 - * write = 刮削(SCR-1):同一接口 write=true,按作品排 scrapeMedia,把 NFO 与图片写进媒体库目录(覆盖方式按设置)。 + * 刷新元数据(§7.2):只对已登记项;多选时按作品去重(POST /files/scrape-batch)。只更新本工具的字段,不写文件。 */ -export function RefreshMetadataDialog({ - items, - write = false, - onClose, -}: { - items: FileEntry[]; - write?: boolean | undefined; - onClose: () => void; -}) { +export function RefreshMetadataDialog({ items, onClose }: { items: FileEntry[]; onClose: () => void }) { const batch = useScrapeBatch(); const key = useMemo(() => newIdempotencyKey(), []); const [result, setResult] = useState(null); @@ -1008,15 +1007,13 @@ export function RefreshMetadataDialog({ const single = entities.length === 1 ? entities[0] : null; const run = () => batch.mutate( - { idempotencyKey: key, mediaEntityIds: entities.map((a) => a?.mediaEntityId as string), write }, + { idempotencyKey: key, mediaEntityIds: entities.map((a) => a?.mediaEntityId as string) }, { onSuccess: (r) => { setResult(r); const skipped = r.skipped.length ? `;${r.skipped.length} 个未提交` : ""; (r.skipped.length ? toast.warning : toast.success)( - write - ? `已提交 ${r.submitted.length} 个作品的刮削${skipped}` - : `已提交 ${r.submitted.length} 个作品,锁定字段不变${skipped}`, + `已提交 ${r.submitted.length} 个作品,锁定字段不变${skipped}`, ); }, onError: (e) => toast.error(filesErrorText(e, "提交失败")), @@ -1027,14 +1024,10 @@ export function RefreshMetadataDialog({ - {write ? "刮削" : "刷新元数据"} + 刷新元数据 {single ? ` · ${single.title}` : ` · ${entities.length} 个作品`} - - {write - ? "把 NFO 和图片写进媒体库目录,已有文件按设置只补缺失或全部覆盖。结果在作品详情的「刮削」里。" - : "按字段从 TMDB 刷新,锁定的字段和路径不变。不写媒体库目录里的文件。"} - + 按字段从 TMDB 刷新,锁定的字段和路径不变。不写目录里的文件。 {!single && !result ? (