import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { query } from "@/lib/db/client"; import { validateCTP } from "@/lib/ctp/validate"; import { renderCTP } from "@/lib/ctp/render"; import type { TempoMapRow } from "@/lib/db/client"; const ParamsSchema = z.object({ id: z.string().uuid("tempo map id must be a UUID"), count_in: z .enum(["true", "false"]) .transform((v) => v === "true") .optional(), }); /** * GET /api/generate?id=[&count_in=true] * * Renders the requested tempo map to a WAV file and streams it back. * The response is cached for 1 hour since tempo maps are immutable once created. */ export async function GET(req: NextRequest) { const raw = Object.fromEntries(req.nextUrl.searchParams); const parsed = ParamsSchema.safeParse(raw); if (!parsed.success) { return NextResponse.json( { error: "Invalid parameters", details: parsed.error.flatten() }, { status: 400 } ); } const { id, count_in } = parsed.data; // Load tempo map from DB const { rows } = await query( "SELECT * FROM tempo_maps WHERE id = $1", [id] ); if (rows.length === 0) { return NextResponse.json({ error: "Tempo map not found" }, { status: 404 }); } const map = rows[0]; const validation = validateCTP(map.ctp_data); if (!validation.success) { return NextResponse.json( { error: "Stored CTP document is invalid", details: validation.errors.flatten() }, { status: 500 } ); } const doc = validation.data; // Render WAV let wav: Buffer; try { wav = renderCTP(doc, { countIn: count_in }); } catch (err) { console.error("[generate] Render error:", err); return NextResponse.json( { error: "Failed to render click track", detail: String(err) }, { status: 500 } ); } // Build a clean filename const safeName = `${doc.metadata.artist} - ${doc.metadata.title}` .replace(/[^\w\s\-]/g, "") .replace(/\s+/g, "_") .slice(0, 80); const filename = `${safeName}_click.wav`; return new NextResponse(wav, { status: 200, headers: { "Content-Type": "audio/wav", "Content-Disposition": `attachment; filename="${filename}"`, "Content-Length": String(wav.byteLength), "Cache-Control": "public, max-age=3600, immutable", }, }); }