import Link from "next/link"; import { notFound } from "next/navigation"; import BrowseHeader from "@/components/BrowseHeader"; import CutBench from "@/components/CutBench"; import ProvenancePanel from "@/components/ProvenancePanel"; import { isCutName, probeAll, readSong } from "@/lib/browse"; import { fmtBytes, fmtDur } from "@/lib/format"; import { buildStatus } from "@/lib/manifest"; import { readProvenance } from "@/lib/provenance"; import { readNotes } from "@/lib/notes"; import { readCompares } from "@/lib/compares"; import LoudnessTable from "@/components/LoudnessTable"; import { cachedLoudness } from "@/lib/loudness"; import { DEFAULT_TARGET } from "@/lib/loudness-types"; import { readSpec } from "@/lib/spec"; import { resolveRendition } from "@/lib/browse"; // Moved out of app/browse/[song]/[cut]/page.tsx unchanged. `/browse//wide` // is now a project id plus a VIEW, resolved by the registry rather than by a // route segment -- the URL is byte-identical either way, which is the point. export default async function CutPage({ id, cutName, search, }: { id: string; cutName: string; search: { v?: string; plan?: string; notes?: string }; }) { const { v, plan, notes: notesParam } = search; if (!isCutName(cutName)) notFound(); const song = await readSong(id); if (!song) notFound(); const cut = song.cuts.find((c) => c.name === cutName); if (!cut) notFound(); const rels = [cut.shipped?.rel, ...cut.variants.map((x) => x.rel)].filter( (r): r is string => !!r, ); const info = await probeAll(id, rels); const durations: Record = {}; for (const [rel, i] of Object.entries(info)) durations[rel] = i.duration; // The recipe is recorded per FILE, so the shipped cut is the one it describes. // A named plan in build.json is a fact; the ?plan= override is the user's // choice where no builder recorded one. const build = cut.shipped ? await buildStatus(id, cut.shipped.rel) : { entry: null, stale: null }; const prov = await readProvenance(id, plan ?? build.entry?.plans[0] ?? null); const notes = await readNotes(id); const compares = await readCompares(id); // Cached figures only. Measuring here would put an ffmpeg decode per file in // front of every navigation to this page. const spec = await readSpec(id); const loudness = await Promise.all( rels.map(async (rel) => { const abs = resolveRendition(id, rel); return { rel, loudness: abs ? await cachedLoudness(abs) : null }; }), ); const target = { lufs: spec.loudness?.targetLufs ?? DEFAULT_TARGET.lufs, truePeak: spec.loudness?.truePeak ?? DEFAULT_TARGET.truePeak, }; return (
{!cut.shipped && cut.variants.length === 0 ? (
Nothing here yet — no {cutName}.mp4 and no variants named for it.{" "} back to {song.id}
) : ( <>
)}
); }