feat(contracts,server,web): files G6–G8 fields and identify S-7 contract
- G6: FileRoot.pendingCount/pendingPartial for download roots (same rule as the unidentified filter, recursive, cached 5 min, invalidated by files.changed) - G7: FileEntry.parsedHint from parseReleasePath (file + parent dirs); the web subline uses it and no longer parses names itself - G8: FileIdentifyCandidate.posterUrl (TMDB poster_path via adapter and catalog.lookup, local entities from metadata.posterPath); thumbnails in the identify panel - S-7 contract: FilesIdentifyResult.remoteTaskId/remotePending, MediaServerInventory.partial, EventType identify.completed with IdentifyCompletedPayload; identify panel (useIdentifyLive) and organize wizard merge remote results by root + path, buffering events that beat the response Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fd2f7bc33d
commit
abc69886ea
26 files changed
+510
-134
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { LoaderCircle, Sparkles, Wand2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { type FileEntry, type FileRoot, filesErrorText, useIdentify } from "~/api/files.ts";
|
||||
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";
|
||||
@@ -10,6 +10,7 @@ import { candidateEntity, LEVEL, reasonsText } from "~/components/organize-wizar
|
||||
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: "综艺" };
|
||||
|
||||
@@ -28,7 +29,7 @@ export function IdentifyPanel({
|
||||
autoRun?: boolean;
|
||||
onOrganize?: (preset: WizardEntity | null) => void;
|
||||
}) {
|
||||
const identify = useIdentify();
|
||||
const identify = useIdentifyLive();
|
||||
const mutate = identify.mutate;
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: 换文件或 autoRun 时重跑一次
|
||||
useEffect(() => {
|
||||
@@ -56,7 +57,7 @@ export function IdentifyPanel({
|
||||
) : identify.error ? (
|
||||
<p className="text-caption text-destructive">{filesErrorText(identify.error, "识别失败")}</p>
|
||||
) : !identify.data ? (
|
||||
<p className="text-caption text-muted-foreground">按文件名解析,匹配本地作品和 TMDB。只读,不改文件。</p>
|
||||
<p className="text-caption text-muted-foreground">按文件名解析,匹配本地作品和 TMDB。只读,不改文件。</p>
|
||||
) : !it ? (
|
||||
<p className="text-caption text-muted-foreground">{isDir ? "目录里没有视频文件。" : "没有识别结果。"}</p>
|
||||
) : (
|
||||
@@ -64,7 +65,7 @@ export function IdentifyPanel({
|
||||
{isDir && (identify.data.items.length > 1 || identify.data.truncated) ? (
|
||||
<p className="text-meta text-muted-foreground">
|
||||
目录里有 {identify.data.items.length}
|
||||
{identify.data.truncated ? "+" : ""} 个视频,这里只显示第一个。整理时逐项确认。
|
||||
{identify.data.truncated ? "+" : ""} 个视频,这里只显示第一个。整理时逐项确认。
|
||||
</p>
|
||||
) : null}
|
||||
<KvList
|
||||
@@ -86,7 +87,7 @@ export function IdentifyPanel({
|
||||
这个源文件有 {it.records.count} 条整理记录
|
||||
{it.records.lastStatus ? (
|
||||
<StatusBadge tone={RECORD_STATUS_TONE[it.records.lastStatus]}>
|
||||
最近:{RECORD_STATUS_LABEL[it.records.lastStatus]}
|
||||
最近:{RECORD_STATUS_LABEL[it.records.lastStatus]}
|
||||
</StatusBadge>
|
||||
) : null}
|
||||
{it.records.lastRecordId ? (
|
||||
@@ -97,7 +98,7 @@ export function IdentifyPanel({
|
||||
</p>
|
||||
) : null}
|
||||
{it.skipped === "below_min_size" ? (
|
||||
<p className="text-caption text-muted-foreground">低于最小体积,没有查候选。</p>
|
||||
<p className="text-caption text-muted-foreground">低于最小体积,没有查候选。</p>
|
||||
) : null}
|
||||
{it.warnings.map((w) => (
|
||||
<p key={w} className="text-caption text-warning">
|
||||
@@ -113,6 +114,17 @@ export function IdentifyPanel({
|
||||
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">
|
||||
|
||||
Reference in new issue
Block a user