import { NextResponse } from 'next/server'; import fs from 'fs-extra'; import path from 'path'; import { execFile } from 'child_process'; import util from 'util'; import { getSpectrogramsPath, ensureSpectrogramsDir } from '@/lib/utils/paths'; import { AnalysisTrack, SpectrogramResponse } from '@/types'; const execFileAsync = util.promisify(execFile); export async function POST(request: Request) { try { const { albumPath, tracks, upc } = await request.json(); if (!albumPath || !tracks || !Array.isArray(tracks)) { return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); } const specRoot = await ensureSpectrogramsDir(); // Use UPC if available, otherwise fallback to album directory name const albumId = upc || path.basename(albumPath); const albumSpecDir = path.join(specRoot, albumId); await fs.ensureDir(albumSpecDir); const results: Record = {}; // Process tracks // We use Promise.all to process in parallel. // If this causes performance issues with sox, we might need to limit concurrency. await Promise.all(tracks.map(async (track: AnalysisTrack) => { if ('error' in track) return; const trackNo = track.common?.track?.no; if (trackNo === undefined || trackNo === null) return; const outputFilename = `${trackNo}.png`; const outputPath = path.join(albumSpecDir, outputFilename); const relativePath = path.join(albumId, outputFilename); const publicUrl = `/api/spectrogram?path=${encodeURIComponent(relativePath)}`; const exists = await fs.pathExists(outputPath); if (!exists) { // Generate const title = `${trackNo} - ${track.common?.title || track.filename}`; const args = [ track.path, '-n', ]; if (process.env.GENERATE_MONO_SPECTROGRAMS) { args.push('remix', '1'); } args.push( 'spectrogram', '-t', title, '-o', outputPath ); try { await execFileAsync('sox', args); results[track.filename] = { exists: true, url: publicUrl }; } catch (e) { console.error(`Failed to generate spectrogram for ${track.filename}:`, e); results[track.filename] = { exists: false }; } } else { results[track.filename] = { exists: true, url: publicUrl }; } })); return NextResponse.json({ results }); } catch (error) { console.error('Error generating spectrograms:', error); return NextResponse.json({ error: 'Failed to generate spectrograms' }, { status: 500 }); } } export async function DELETE() { try { const specPath = getSpectrogramsPath(); await fs.remove(specPath); await fs.ensureDir(specPath); return NextResponse.json({ success: true }); } catch (error) { console.error('Error purging spectrograms:', error); return NextResponse.json({ error: 'Failed to purge spectrograms' }, { status: 500 }); } } export async function GET(request: Request) { const { searchParams } = new URL(request.url); const relativePath = searchParams.get('path'); if (!relativePath) { return NextResponse.json({ error: 'Missing path' }, { status: 400 }); } const specRoot = getSpectrogramsPath(); const fullPath = path.join(specRoot, relativePath); // Security check: ensure fullPath is within specRoot (prevent directory traversal) const resolvedPath = path.resolve(fullPath); const resolvedRoot = path.resolve(specRoot); if (!resolvedPath.startsWith(resolvedRoot)) { return NextResponse.json({ error: 'Access denied' }, { status: 403 }); } if (!await fs.pathExists(resolvedPath)) { return NextResponse.json({ error: 'File not found' }, { status: 404 }); } const fileBuffer = await fs.readFile(resolvedPath); return new NextResponse(fileBuffer, { headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=31536000, immutable', }, }); }