import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { getTempoMapsForSong, getSongByMbid, insertTempoMap, upsertSong } from "@/lib/db/client"; import { validateCTP } from "@/lib/ctp/validate"; import { getMusicBrainzRecording } from "@/lib/musicbrainz/client"; // ─── GET /api/tracks?mbid= ───────────────────────────────────────────── const GetSchema = z.object({ mbid: z.string().uuid(), }); export async function GET(req: NextRequest) { const params = Object.fromEntries(req.nextUrl.searchParams); const parsed = GetSchema.safeParse(params); if (!parsed.success) { return NextResponse.json( { error: "mbid (UUID) is required" }, { status: 400 } ); } const maps = await getTempoMapsForSong(parsed.data.mbid); return NextResponse.json({ maps }); } // ─── POST /api/tracks ───────────────────────────────────────────────────────── // Body: a raw CTP document JSON export async function POST(req: NextRequest) { let body: unknown; try { body = await req.json(); } catch { return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); } const validation = validateCTP(body); if (!validation.success) { return NextResponse.json( { error: "CTP document validation failed", details: validation.errors.flatten(), }, { status: 422 } ); } const doc = validation.data; if (!doc.metadata.mbid) { return NextResponse.json( { error: "CTP document must include a metadata.mbid to be stored" }, { status: 422 } ); } // Ensure the song exists — auto-register it if not const existing = await getSongByMbid(doc.metadata.mbid); if (!existing) { try { const mbRecord = await getMusicBrainzRecording(doc.metadata.mbid); await upsertSong({ mbid: doc.metadata.mbid, title: mbRecord.title, artist: mbRecord.artist, duration_seconds: mbRecord.duration_seconds, acousticbrainz_bpm: mbRecord.bpm, acousticbrainz_time_sig_num: mbRecord.timeSigNum, source: "musicbrainz", }); } catch { // MusicBrainz unreachable — fall back to CTP metadata await upsertSong({ mbid: doc.metadata.mbid, title: doc.metadata.title, artist: doc.metadata.artist, duration_seconds: doc.metadata.duration_seconds, acousticbrainz_bpm: null, acousticbrainz_time_sig_num: null, source: "manual", }); } } const map = await insertTempoMap({ song_mbid: doc.metadata.mbid, ctp_data: body as Record, contributed_by: doc.metadata.contributed_by, }); return NextResponse.json({ map }, { status: 201 }); }