- 永久删除的说明改成实际开关 FILES_ALLOW_PERMANENT_DELETE,去掉「在设置里开启」的错误说法 - 去掉「待处理」「仅 TMDB」「目录根」等旧称,收录版本退回状态写「需处理」 - 删除、移动/复制、重命名、刮削、刷新元数据对话框去掉复述界面的说明 - 加载与进行中文字统一带省略号;Toast 只写结果 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
165 lines
7.3 KiB
TypeScript
165 lines
7.3 KiB
TypeScript
import { LoaderCircle, Sparkles, Wand2 } from "lucide-react";
|
|
import { useEffect } from "react";
|
|
import { Link } from "react-router";
|
|
import { type FileEntry, type FileRoot, filesErrorText } from "~/api/files.ts";
|
|
import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE } from "~/api/records.ts";
|
|
import { DetailSection } from "~/components/console/detail-sheet.tsx";
|
|
import { KvList } from "~/components/console/kv-list.tsx";
|
|
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
|
import { candidateEntity, LEVEL, reasonsText } from "~/components/organize-wizard/entity-picker.tsx";
|
|
import type { WizardEntity } from "~/components/organize-wizard/model.ts";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import { contentUnitLabel } from "./model.ts";
|
|
import { useIdentifyLive } from "./use-identify-live.ts";
|
|
|
|
const KIND: Record<string, string> = { movie: "电影", series: "剧集", variety: "综艺" };
|
|
|
|
/**
|
|
* 详情抽屉"识别"节(§7.1):解析字段 + Top5 候选(等级、理由),每个候选 `按此整理` 打开向导并预填作品。
|
|
* 识别只读,不写库。autoRun:从菜单"识别"进入时自动跑一次。
|
|
*/
|
|
export function IdentifyPanel({
|
|
root,
|
|
entry,
|
|
autoRun,
|
|
onOrganize,
|
|
}: {
|
|
root: FileRoot;
|
|
entry: FileEntry;
|
|
autoRun?: boolean;
|
|
onOrganize?: (preset: WizardEntity | null) => void;
|
|
}) {
|
|
const identify = useIdentifyLive();
|
|
const mutate = identify.mutate;
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: 换文件或 autoRun 时重跑一次
|
|
useEffect(() => {
|
|
identify.reset();
|
|
if (autoRun) mutate({ items: [{ root: root.id, path: entry.path }], minSizeBytes: 0 });
|
|
}, [autoRun, root.id, entry.path]);
|
|
const run = () => mutate({ items: [{ root: root.id, path: entry.path }], minSizeBytes: 0 });
|
|
const it = identify.data?.items[0];
|
|
const isDir = entry.type === "dir";
|
|
|
|
return (
|
|
<DetailSection
|
|
title="识别"
|
|
actions={
|
|
<Button size="xs" variant="outline" onClick={run} disabled={identify.isPending}>
|
|
{identify.isPending ? <LoaderCircle className="animate-spin" /> : <Sparkles />}
|
|
{identify.data ? "重新识别" : "识别"}
|
|
</Button>
|
|
}
|
|
>
|
|
{identify.isPending ? (
|
|
<p className="flex items-center gap-2 text-caption text-muted-foreground">
|
|
<LoaderCircle className="h-3.5 w-3.5 animate-spin" /> 匹配本地作品和 TMDB…
|
|
</p>
|
|
) : identify.error ? (
|
|
<p className="text-caption text-destructive">{filesErrorText(identify.error, "识别失败")}</p>
|
|
) : !identify.data ? (
|
|
<p className="text-caption text-muted-foreground">按文件名匹配本地作品和 TMDB,不改文件。</p>
|
|
) : !it ? (
|
|
<p className="text-caption text-muted-foreground">{isDir ? "目录里没有视频文件。" : "没有识别结果。"}</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{isDir && (identify.data.items.length > 1 || identify.data.truncated) ? (
|
|
<p className="text-meta text-muted-foreground">
|
|
目录里有 {identify.data.items.length}
|
|
{identify.data.truncated ? "+" : ""} 个视频,这里只显示第一个。整理时逐项确认。
|
|
</p>
|
|
) : null}
|
|
<KvList
|
|
items={[
|
|
{ label: "标题", value: it.parsed.title },
|
|
{ label: "年份", value: it.parsed.year },
|
|
{
|
|
label: "季集",
|
|
value: it.parsed.season != null ? contentUnitLabel(it.parsed.season, it.parsed.episodes) : null,
|
|
},
|
|
{ label: "类型", value: it.parsed.mediaKind ? (KIND[it.parsed.mediaKind] ?? it.parsed.mediaKind) : null },
|
|
{ label: "分辨率", value: it.parsed.resolution },
|
|
{ label: "来源", value: it.parsed.source },
|
|
{ label: "发布组", value: it.parsed.releaseGroup },
|
|
]}
|
|
/>
|
|
{it.records?.count ? (
|
|
<p className="flex flex-wrap items-center gap-1.5 text-caption">
|
|
这个源文件有 {it.records.count} 条整理记录
|
|
{it.records.lastStatus ? (
|
|
<StatusBadge tone={RECORD_STATUS_TONE[it.records.lastStatus]}>
|
|
最近:{RECORD_STATUS_LABEL[it.records.lastStatus]}
|
|
</StatusBadge>
|
|
) : null}
|
|
{it.records.lastRecordId ? (
|
|
<Link className="underline underline-offset-2" to={`/console/organize/${it.records.lastRecordId}`}>
|
|
查看
|
|
</Link>
|
|
) : null}
|
|
</p>
|
|
) : null}
|
|
{it.skipped === "below_min_size" ? (
|
|
<p className="text-caption text-muted-foreground">文件小于最小体积,没有查找候选。</p>
|
|
) : null}
|
|
{it.warnings.map((w) => (
|
|
<p key={w} className="text-caption text-warning">
|
|
{w}
|
|
</p>
|
|
))}
|
|
{it.candidates.length ? (
|
|
<ul className="divide-y divide-border/60 rounded-xl border border-border">
|
|
{it.candidates.map((c) => {
|
|
const e = candidateEntity(c);
|
|
return (
|
|
<li
|
|
key={`${c.provider}-${c.mediaEntityId ?? c.externalId}`}
|
|
className="flex items-start gap-2 px-3 py-2"
|
|
>
|
|
{c.posterUrl ? (
|
|
<img
|
|
src={c.posterUrl}
|
|
alt=""
|
|
loading="lazy"
|
|
referrerPolicy="no-referrer"
|
|
className="h-12 w-8 shrink-0 rounded-sm bg-muted object-cover"
|
|
/>
|
|
) : (
|
|
<span aria-hidden className="h-12 w-8 shrink-0 rounded-sm bg-muted" />
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
<p className="flex flex-wrap items-center gap-1.5 text-caption text-foreground">
|
|
<span className="truncate">
|
|
{c.title}
|
|
{c.year ? ` (${c.year})` : ""}
|
|
</span>
|
|
<StatusBadge tone={LEVEL[c.level]?.tone}>置信 {LEVEL[c.level]?.label ?? c.level}</StatusBadge>
|
|
{c.mediaEntityId ? null : <StatusBadge tone="warning">不在媒体库</StatusBadge>}
|
|
</p>
|
|
<p className="text-meta text-muted-foreground">
|
|
{[
|
|
KIND[c.kind] ?? c.kind,
|
|
c.externalId ? `${c.provider.toUpperCase()} ${c.externalId}` : null,
|
|
reasonsText(c.reasons),
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · ")}
|
|
</p>
|
|
</div>
|
|
{onOrganize && e ? (
|
|
<Button size="xs" variant="outline" onClick={() => onOrganize(e)}>
|
|
<Wand2 />
|
|
按此整理
|
|
</Button>
|
|
) : null}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
) : it.skipped ? null : (
|
|
<p className="text-caption text-muted-foreground">没有可信候选。整理时可以手动搜索作品。</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</DetailSection>
|
|
);
|
|
}
|