import { type AssetVersion, matchGapTexts, type SeasonVersions, type UnitMatch, type VersionSummary, } from "@umt/contracts"; import { ChevronDown } from "lucide-react"; import { type ReactNode, useState } from "react"; import { useLibraryVersions } from "~/api/library-versions.ts"; import { type KvItem, KvList } from "~/components/console/kv-list.tsx"; import { PaginatedList } from "~/components/console/paginated-list.tsx"; import { RecordLink } from "~/components/console/record-sheet.tsx"; import { TechInfo } from "~/components/console/tech-info.tsx"; import { Panel } from "~/components/library/parts.tsx"; import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts"; // LIBV-1 入库版本:每集一行规格(分辨率 · 来源 · 编码 · HDR · 制作组 · 大小);季头一行汇总。 // POLISH-1:点整行展开,多个版本同级并列,选中的版本下方才显示路径、音轨、字幕、时长; // 规格来源、探测原始值、比对原文放进 TechInfo;与收录版本的差距用 matchGapTexts 转成人话。 const SOURCE_LABEL: Record = { REMUX: "Remux", "BluRay-Disc": "原盘", BluRay: "蓝光压制", }; const CODEC_LABEL: Record = { "H.265": "HEVC" }; const LANG_LABEL: Record = { "zh-Hans": "简中", "zh-Hant": "繁中", zh: "中文", yue: "粤语", en: "英语", ja: "日语", ko: "韩语", fr: "法语", de: "德语", es: "西语", }; export const sourceLabel = (s: string | null) => (s ? (SOURCE_LABEL[s] ?? s) : null); const langLabel = (l: string) => LANG_LABEL[l] ?? l; /** DV 带 HDR10 兜底时只写 DV */ export function hdrLabel(hdr: readonly string[]): string | null { const shown = hdr.includes("DV") ? hdr.filter((h) => h !== "HDR10") : hdr; return shown.length ? shown.join("/") : null; } /** 一行规格:2160p · WEB-DL · HEVC · DV · FLUX · 13.2 GB */ export function versionParts(v: AssetVersion): string[] { return [ v.resolution, sourceLabel(v.source), v.videoCodec ? (CODEC_LABEL[v.videoCodec] ?? v.videoCodec) : null, hdrLabel(v.hdr), v.edition, v.releaseGroup, v.sizeBytes !== null ? formatBytes(v.sizeBytes, 1) : null, ].filter((x): x is string => !!x); } /** 季 / 作品汇总:2160p WEB-DL DV ×8 · 1080p 蓝光压制 ×2 · 共 78 GB */ export function summaryText(s: VersionSummary): string { const specs = s.specs.map((g) => { const label = [g.resolution, sourceLabel(g.source), hdrLabel(g.hdr)].filter(Boolean).join(" ") || "规格未知"; return s.specs.length > 1 ? `${label} ×${g.count}` : label; }); const shown = specs.length > 2 ? [...specs.slice(0, 2), `等 ${specs.length} 种`] : specs; return [...shown, s.sizeBytes ? `共 ${formatBytes(s.sizeBytes, 1)}` : null].filter(Boolean).join(" · "); } export function seasonLine(s: SeasonVersions, withMatch = true): string | null { if (s.summary.versionCount === 0) return null; const count = `${s.episodesWithVersion}/${s.episodeTotal ?? "?"} 集`; const up = withMatch && s.matchCounts?.upgradeable ? `${s.matchCounts.upgradeable} 集可升级` : null; return [count, summaryText(s.summary), up].filter(Boolean).join(" · "); } function audioText(v: AssetVersion): string | null { if (!v.audio.length) return null; return v.audio .map((a) => [a.codec, a.channels, a.language ? langLabel(a.language) : null].filter(Boolean).join(" ")) .join(";"); } function duration(sec: number | null): string | null { if (!sec) return null; const h = Math.floor(sec / 3600); const m = Math.round((sec % 3600) / 60); return h ? `${h} 小时 ${m} 分钟` : `${m} 分钟`; } const SPEC_FROM: Record = { name: "文件名", probe: "ffprobe" }; /** 与收录版本的差距 → 人话短句("可换成 2160p"、"缺 HDR 版本");原文留给技术信息 */ export function gapLine(match: UnitMatch | undefined): string | null { if (match?.state !== "upgradeable") return null; const texts = matchGapTexts(match.codes); return texts.length ? texts.join(" · ") : "还有更好的版本可换"; } /** 选中版本的详情:路径、音轨、字幕、时长;规格来源、探测原始值、比对原文进「技术信息」 */ function VersionDetail({ v, match }: { v: AssetVersion; match?: UnitMatch | undefined }) { const items: KvItem[] = [ { label: "文件", value: v.absolutePath ?? v.relativePath ?? v.fileName, mono: true, wide: true }, { label: "音轨", value: audioText(v) }, { label: "字幕", value: v.subtitleLanguages.map(langLabel).join("、") || null }, { label: "时长", value: duration(v.durationSec) }, { label: "色深", value: v.bitDepth ? `${v.bitDepth} bit` : null }, { label: "整理时间", value: formatDateTime(v.organizedAt) }, { label: "下载任务", value: v.downloadTaskId ? ( 查看下载任务 ) : null, }, ].filter((it) => it.value !== null && it.value !== undefined && it.value !== ""); const ownMatch = match && match.assetId === v.assetId ? match : undefined; return (
SPEC_FROM[f] ?? f).join(" + ") || null }, { label: "比对说明", value: ownMatch?.reasons.join(";") || null, wide: true }, { label: "原因码", value: ownMatch?.codes.join(", ") || null, wide: true }, ]} > {v.probe ? (
            {JSON.stringify(v.probe, null, 2)}
          
) : null}
); } /** 一个版本的规格一行:2160p · WEB-DL · HEVC · DV · FLUX · 13.2 GB + 状态 */ export function VersionSpecLine({ v, best, aside }: { v: AssetVersion; best?: boolean; aside?: ReactNode }) { const parts = versionParts(v); const missing = v.status === "missing" || (v.locationStatus !== null && v.locationStatus !== "available"); return ( {parts.length ? ( parts.map((p, i) => ( {i > 0 ? · : null} {p} )) ) : ( {v.fileName} )} {best ? 符合收录版本 : null} {missing ? 文件不在原位置 : null} {aside} ); } const ROW_CLASS = "flex w-full min-w-0 cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors duration-micro hover:bg-foreground/[0.05] focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none"; /** * 版本列表(POLISH-1):每个版本一行同级并列,点一行选中并在它下方展开详情,再点收起。 * 电影面板、没对上集数的文件都用它;剧集每集先收成一行,见 EpisodeVersionList。 */ export function VersionList({ versions, match, initial, onCollapse, compact = false, }: { versions: AssetVersion[]; match?: UnitMatch | undefined; /** 初始选中的版本;缺省不选 */ initial?: string | null; /** 收起选中版本时回调(剧集用来收回到一行) */ onCollapse?: () => void; compact?: boolean; }) { const [selected, setSelected] = useState(initial ?? null); const tagBest = match?.state === "satisfied" && versions.length > 1; return ( {(v) => { const on = selected === v.assetId; return (
  • {on ? : null}
  • ); }}
    ); } /** * 一集的版本:收起时一行(达标或最接近的版本 + "N 个版本"),点整行展开; * 展开后每个版本一行同级并列,选中的版本下方显示文件路径、音轨、字幕等。 * 与收录版本的差距用人话写在下面("可换成 2160p · 缺 HDR 版本")。 */ export function EpisodeVersionList({ versions, match, className, }: { versions: AssetVersion[]; match?: UnitMatch | undefined; className?: string; }) { const [open, setOpen] = useState(false); if (!versions.length) return null; const lead = versions.find((v) => v.assetId === match?.assetId) ?? versions[0]!; const ordered = [lead, ...versions.filter((v) => v !== lead)]; const gap = gapLine(match); return (
    {open ? ( setOpen(false)} /> ) : ( )} {gap ?

    {gap}

    : null}
    ); } /** 电影的入库版本面板:每个文件一行,附与收录版本的比对 */ export function MovieVersionsPanel({ entityId }: { entityId: string }) { const q = useLibraryVersions(entityId); const data = q.data; if (!data?.movie || data.movie.versions.length === 0) return null; const match = data.movie.matches?.[0]; const target = data.targets[0]; const versions = [...data.movie.versions].sort( (a, b) => Number(b.assetId === match?.assetId) - Number(a.assetId === match?.assetId), ); const state = match?.state === "satisfied" ? `符合「${target?.name}」` : gapLine(match); return (
    ); } /** 剧集里没对上具体集数的文件 */ export function UnplacedVersions({ versions }: { versions: AssetVersion[] }) { const [open, setOpen] = useState(false); if (!versions.length) return null; return (
    {open ? (
    ) : null}
    ); }