import Link from "next/link"; import { notFound } from "next/navigation"; import BrowseHeader from "@/components/BrowseHeader"; import VerdictChip from "@/components/VerdictChip"; import SpecSheet from "@/components/SpecSheet"; import NoteField from "@/components/NoteField"; import CopyButton from "@/components/CopyButton"; import { readSong, probeAll, thumbAliasesFor, type Cut, type Song } from "@/lib/browse"; import ThumbBench from "@/components/ThumbBench"; import { thumbView } from "@/lib/thumbs"; import { fmtAgo, fmtBytes, fmtDur } from "@/lib/format"; import { Markdown } from "@/lib/markdown"; import { operationsFor, readSpec, validateSpec } from "@/lib/spec"; import { readNotes, type NoteMap } from "@/lib/notes"; import { TRIM_SETS } from "@/lib/trim"; // Moved out of app/browse/[song]/page.tsx unchanged, because [song] and // [...path] cannot both be dynamic segments at the same level. The dispatcher // in ProjectView.tsx hands it an id; nothing else about the page differs. export default async function SongProject({ id }: { id: string }) { const song = await readSong(id); if (!song) notFound(); // Durations are worth an ffprobe HERE but not on the index: this page is a // handful of files and "which of these is the short cut" is exactly the // question it answers. Memoised by mtime in lib/browse.ts. const rels = [ ...song.cuts.flatMap((c) => [c.shipped?.rel, ...c.variants.map((v) => v.rel)]), ...song.unattributed.map((v) => v.rel), ].filter((r): r is string => !!r); const info = await probeAll(id, rels); const spec = await readSpec(id); const problems = await validateSpec(spec); const notes = await readNotes(id); // Two small JSON reads, no probing -- the bench draws what the manifests say. const thumbs = await thumbView(id, thumbAliasesFor(id)); return (
{/* The one obvious place to write about the song, open by default -- everything else on the page is collapsed until asked for. */}
{song.cuts.map((cut) => ( ))} {song.unattributed.length > 0 && (
matches no cut name — attribute by renaming, never by guessing
    {song.unattributed.map((v) => (
  • {v.rel}{" "} {fmtBytes(v.size)}
  • ))}
)}
); } function CutCard({ song, cut, info, notes, }: { song: Song; cut: Cut; info: Record; notes: NoteMap; }) { const shipped = cut.shipped; const live = cut.variants.filter((v) => !v.retired); const retired = cut.variants.filter((v) => v.retired); return (
{shipped ? ( /* eslint-disable-next-line @next/next/no-img-element */ ) : (
not built
)}
{cut.name} {shipped ? ( {fmtDur(info[shipped.rel]?.duration ?? 0)} ) : ( — a hole in the set )} {shipped && info[shipped.rel] && ( {info[shipped.rel].width}×{info[shipped.rel].height} )} {shipped ? `${fmtBytes(shipped.size)} · ${fmtAgo(shipped.mtimeMs)}` : ""}
{shipped && ( )} {/* Two different notes, deliberately. The `cut:` one is about the SLOT -- it survives a promote and can be written about a cut that has not been built. The `file:` one is about these bytes. */} {shipped && ( )} {live.length > 0 && (
    {live.map((v) => (
  • {v.tag} {fmtDur(info[v.rel]?.duration ?? 0)} {fmtBytes(v.size)}
  • ))}
)} {retired.length > 0 && (
{retired.length} retired: {retired.map((v) => v.tag).join(", ")}
)}
); }