import { readSong, resolveRendition, type Song } from "@/lib/browse"; import { cachedLoudness, measureLoudness } from "@/lib/loudness"; import { DEFAULT_TARGET, type LoudnessTarget } from "@/lib/loudness-types"; import { readSpec } from "@/lib/spec"; export const dynamic = "force-dynamic"; // GET reads the disk cache and measures NOTHING. POST measures exactly one file. // // That split is the whole design. A four-minute render takes seconds to decode, // and a page that measured every rendition on load would put half a minute in // front of a cut page -- the same trap listSongs() avoids by never probing, at // a heavier weight. So the table renders "not measured" and fills in behind a // button somebody pressed. const relsOf = (song: Song): string[] => [...song.cuts.flatMap((c) => [c.shipped, ...c.variants]), ...song.unattributed] .filter((r): r is NonNullable => !!r) .map((r) => r.rel); async function targetFor(id: string): Promise { const spec = await readSpec(id); return { lufs: spec.loudness?.targetLufs ?? DEFAULT_TARGET.lufs, truePeak: spec.loudness?.truePeak ?? DEFAULT_TARGET.truePeak, }; } export async function GET(request: Request) { const url = new URL(request.url); const id = url.searchParams.get("song") ?? ""; const only = url.searchParams.getAll("rel"); const song = await readSong(id); if (!song) return Response.json({ error: "no such song" }, { status: 404 }); const known = relsOf(song); const rels = only.length ? only.filter((r) => known.includes(r)) : known; const rows = await Promise.all( rels.map(async (rel) => { const abs = resolveRendition(id, rel); return { rel, loudness: abs ? await cachedLoudness(abs) : null }; }), ); return Response.json( { song: id, target: await targetFor(id), rows }, { headers: { "cache-control": "no-store" } }, ); } export async function POST(request: Request) { let body: { song?: string; rel?: string }; try { body = await request.json(); } catch { return Response.json({ error: "bad json" }, { status: 400 }); } const { song: id, rel } = body; if (!id || !rel) return Response.json({ error: "song and rel are required" }, { status: 400 }); const song = await readSong(id); if (!song) return Response.json({ error: "no such song" }, { status: 404 }); if (!relsOf(song).includes(rel)) { return Response.json({ error: "not a file in this song" }, { status: 404 }); } const abs = resolveRendition(id, rel); if (!abs) return Response.json({ error: "outside the roots" }, { status: 400 }); try { const loudness = await measureLoudness(abs); return Response.json( { rel, loudness, target: await targetFor(id) }, { headers: { "cache-control": "no-store" } }, ); } catch (e) { // A measurement that did not happen must read as one that did not happen. return Response.json( { error: e instanceof Error ? e.message : String(e) }, { status: 500 }, ); } }