import type {
ContentUnit,
EntityMetadataField,
ExternalIdentity,
LibraryAssetView,
LibraryDetail,
LibraryDownloadView,
MediaEntityDetail,
} from "@umt/contracts";
import { EntityMetadataField as FieldEnum, FinalFieldValue } from "@umt/contracts";
import {
ArrowLeft,
CalendarClock,
CalendarPlus,
ChevronRight,
ExternalLink,
FolderTree,
GitMerge,
Lock,
LockOpen,
PackageSearch,
PencilLine,
RefreshCw,
SearchX,
} from "lucide-react";
import { useState } from "react";
import { Link, useParams } from "react-router";
import { toast } from "sonner";
import { mediaServers } from "~/api/instances.ts";
import {
useEntityDetail,
useLibraryDetail,
useLibraryRoots,
useRefreshMetadata,
useUnlockField,
} from "~/api/library.ts";
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { evidenceLabel, FieldLockDialog, IdentityDialog, MergeDialog } from "~/components/library/entity-dialogs.tsx";
import {
CONTENT_TYPE,
DOWNLOAD_STATUS,
ENTITY_FIELD_LABEL,
ENTITY_STATUS,
IDENTITY_STATUS,
LOCATION_KIND,
LOCATION_STATUS,
MEDIA_KIND,
METADATA_SOURCE,
ORG_STATUS,
SEGMENT,
UNIT_KIND,
VERIFY_STATUS,
} from "~/components/library/labels.ts";
import {
Chip,
ChipList,
CoverPlaceholder,
Panel,
ProgressLine,
posterPathOf,
relTime,
Tone,
} from "~/components/library/parts.tsx";
import { SlotCard } from "~/components/library/slot-card.tsx";
import { EntityNamingButton } from "~/components/naming/entity-naming-dialog.tsx";
import { useReleaseSearchLauncher } from "~/components/search/search-sections.tsx";
import { EmbyBadge } from "~/components/search/work-card.tsx";
import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-form.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
// 作品详情(REQ §13.1 / §13.4.3):实际路径、外部身份映射及状态、元数据来源、各 Slot、候选拒绝理由、
// 内容单元、下载历史与校验结果,同一页同时展示。
const pad = (n: number) => String(n).padStart(2, "0");
function unitCode(u: ContentUnit): string {
const seg = SEGMENT[u.segment] ? `(${SEGMENT[u.segment]})` : "";
if (u.kind === "season") return u.seasonNumber !== null ? `S${pad(u.seasonNumber)}` : "季";
if (u.kind === "episode") {
const s = u.seasonNumber !== null ? `S${pad(u.seasonNumber)}` : "";
const e = u.episodeNumber !== null ? `E${pad(u.episodeNumber)}` : "";
return `${s}${e}${seg}` || (u.sourceIssue ?? "集");
}
return `${UNIT_KIND[u.kind] ?? u.kind}${u.episodeNumber !== null ? ` ${u.episodeNumber}` : ""}${seg}`;
}
function deliveryLine(a: LibraryAssetView["asset"]): string {
const d = a.delivery;
return [
d.resolution,
d.sourceType,
d.videoCodec,
d.hdr?.join("/"),
d.releaseGroup,
a.contentEdition ? `版本 ${a.contentEdition}` : null,
a.sizeBytes !== null ? formatBytes(a.sizeBytes, 1) : null,
]
.filter(Boolean)
.join(" · ");
}
// ── 实际路径与库根 ──
function PathsPanel({ detail }: { detail: LibraryDetail }) {
const roots = useLibraryRoots();
const rootOf = (id: string | null) => roots.data?.items.find((r) => r.id === id);
const bindings = detail.pathBindings.filter((b) => b.status !== "superseded");
return (
{root ? `${root.localPath.replace(/\/+$/, "")}/` : ""}
{b.relativePath}
{l.absolutePath ?? l.relativePath}
{bindings.map((b) => {
const root = rootOf(b.libraryRootId);
return (
) : null}
{detail.assets.map((a) => (
{a.locations.map((l) => (
{list.map((i) => (
)}
证据 {evidenceLabel(i.evidenceLevel)} · {VERIFY_STATUS[i.verifyStatus]?.label ?? i.verifyStatus} {i.verifiedAt ? `(${relTime(i.verifiedAt)})` : ""} · r{i.revision}
{reason ?{reason}
: null}
证据 {evidenceLabel(ev.evidenceLevel)}
{ev.sourceUrl && host ? (
{host}
) : null}
{final ? ( Array.isArray(final.value) ? ( final.value.join("、") ) : ( String(final.value ?? "—") ) ) : ( 未提供 )}
{final ? (来源 {METADATA_SOURCE[final.source] ?? final.source} {final.language ? ` · ${final.language}` : ""} · {relTime(final.at)}
) : null} {final ?候选: {cands.map((c) => `${METADATA_SOURCE[c.source] ?? c.source}「${String(c.value)}」`).join(" / ")}
) : null}别名(用于模糊召回)
{u.title ?? u.sourceIssue ?? "—"}
{UNIT_KIND[u.kind]} · {CONTENT_TYPE[u.contentType]} {u.airDate ? ` · ${u.airDate}` : ""}
{d.plan ? `${d.plan.purpose === "upgrade" ? "升级" : d.plan.purpose === "fill" ? "补缺" : "换源"}计划 · ` : ""} {formatDateTime(d.downloadTask.createdAt)}
旧 ID {redirected} 已跳转到当前作品。
) : null}{MEDIA_KIND[e.kind]} {e.year ? ` · ${e.year}` : ""} {e.originalTitle ? ` · ${e.originalTitle}` : ""}
{e.id}
{(ms.data?.items.length ?? 0) > 0 ? (