feat(web): 文件管理「刮削…」对任意目录可用,候选选择与刮削结果(SCR-2)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-01 00:08:17 +08:00
1 parent 87bf33b42b
commit d8ae5e179c
8 files changed
+426 -126

No files matched your search

+84 -6
View File
@@ -12,6 +12,7 @@ import type {
FilesListResponse,
FilesOrganizeGroup,
FilesTransferResult,
ScrapeRecord,
TrashItem,
} from "@umt/contracts";
import type { MediaSearchResult } from "~/api/files.ts";
@@ -699,6 +700,82 @@ function identify(
let seq = 0;
// SCR-2 刮削:认得出的(KNOWN)直接写,认不出的给两个 TMDB 候选
const SCRAPES = new Map<string, ScrapeRecord>();
function mockScrape(b: {
items: { root: string; path: string; mediaEntityId?: string; external?: { externalId: string } }[];
mode?: "missing" | "overwrite";
}) {
const records: ScrapeRecord[] = [];
const needsChoice: unknown[] = [];
for (const it of b.items) {
const known = KNOWN.find((k) => k.match.test(it.path) || k.id === it.mediaEntityId);
if (!known && !it.external) {
needsChoice.push({
root: it.root,
path: it.path,
title: it.path.split("/").pop() ?? it.path,
warnings: [],
candidates: [1972, 2002].map((year, i) => ({
mediaEntityId: null,
provider: "tmdb",
externalId: String(593 + i),
title: it.path.split("/").pop() ?? it.path,
year,
kind: "movie",
level: "medium",
score: 5 - i,
reasons: ["title:exact"],
posterUrl: null,
})),
});
continue;
}
const stem = it.path.replace(/\.[^./]+$/, "");
const now = new Date().toISOString();
const files = [
{ path: `${stem}.nfo`, kind: "nfo" as const, role: "movie_nfo", action: "written" as const, reason: null },
{
path: `${it.path}/poster.jpg`,
kind: "image" as const,
role: "poster",
action: "written" as const,
reason: null,
},
];
const rec: ScrapeRecord = {
id: `00000000-0000-4000-8000-${String(++seq).padStart(12, "0")}`,
root: it.root,
path: it.path,
status: "succeeded",
trigger: "manual",
mode: b.mode ?? "missing",
match: {
mediaEntityId: known?.id ?? null,
provider: "tmdb",
externalId: it.external?.externalId ?? "0",
kind: known?.kind === "series" ? "tv" : "movie",
title: known?.title ?? it.path,
year: known?.year ?? null,
source: it.external || it.mediaEntityId ? "chosen" : known ? "auto" : "chosen",
},
written: files.length,
skipped: 0,
failed: 0,
reason: null,
files,
organizationRecordId: null,
taskId: `run-${seq}`,
requestedAt: now,
startedAt: now,
finishedAt: now,
};
SCRAPES.set(rec.id, rec);
records.push(rec);
}
return { records, needsChoice, skipped: [], pending: false, entities: [] };
}
function doMove(body: {
items: ItemRef[];
target: ItemRef;
@@ -1119,13 +1196,14 @@ export async function mockFiles(path: string, arg: FetcherArg): Promise<unknown>
await delay(400);
return organize(b);
case "/files/scrape":
await delay(500);
return mockScrape(b);
case "/files/scrape-records":
return {
mediaEntityId: b.mediaEntityId,
taskId: `run-${++seq}`,
lockedFields: ["title"],
nfo: b.writeNfo ? "queued" : "not_requested",
images: b.images ? "queued" : "not_requested",
scrapeTaskId: b.writeNfo || b.images ? `run-${++seq}` : null,
records: String(p.ids ?? "")
.split(",")
.map((id) => SCRAPES.get(id))
.filter(Boolean),
};
case "/files/probe":
await delay(600);