feat(web): 文件管理「刮削…」对任意目录可用,候选选择与刮削结果(SCR-2)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
87bf33b42b
commit
d8ae5e179c
8 files changed
+426
-126
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user