127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
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<string, { exists: boolean, url?: string }> = {};
|
|
|
|
// 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',
|
|
},
|
|
});
|
|
}
|