import { readSong, readVerdicts, verdictsFile, isVerdict, type VerdictEntry } from "@/lib/browse"; import { withStateLock, writeJsonAtomic } from "@/lib/state"; export const dynamic = "force-dynamic"; // Record a judgement against one file. // // The verdicts live WITH THE DELIVERABLES -- videos//verdicts.json -- // rather than in umtool/song/ where the clip state lives. A tree of songs is a // thing that gets copied and moved, and its judgements should travel with it. // That is the only reason this does not use stateFile(). // // The write is still read-modify-write under withStateLock + writeJsonAtomic, // because the CLI scripts may read this file and a torn read of a judgement is // a lost one. export async function POST(request: Request) { let body: { song?: string; rel?: string; verdict?: string; note?: string }; try { body = await request.json(); } catch { return Response.json({ error: "bad json" }, { status: 400 }); } const { song: id, rel, verdict, note } = body; if (!id || !rel) return Response.json({ error: "song and rel are required" }, { status: 400 }); if (!verdict || !isVerdict(verdict)) { return Response.json({ error: "verdict must be keep, reject or undecided" }, { status: 400 }); } const song = await readSong(id); if (!song) return Response.json({ error: "no such song" }, { status: 404 }); // Membership in the current scan is the gate. A well-formed name for a file // that is not there would otherwise accumulate judgements about nothing. const known = [ ...song.cuts.flatMap((c) => [c.shipped, ...c.variants]), ...song.unattributed, ].some((r) => r && r.rel === rel); if (!known) return Response.json({ error: "not a file in this song" }, { status: 404 }); const entry: VerdictEntry = { verdict, at: new Date().toISOString(), ...(note?.trim() ? { note: note.trim() } : {}), }; await withStateLock(async () => { const map = await readVerdicts(id); if (verdict === "undecided" && !entry.note) { // Undecided with nothing to say is the default. Storing it would grow the // file with rows that mean "no judgement", and make an untouched tree // indistinguishable from one somebody has been through. delete map[rel]; } else { map[rel] = entry; } await writeJsonAtomic(verdictsFile(id), map); }); return Response.json({ ok: true, rel, entry }, { headers: { "cache-control": "no-store" } }); }