Restart repository history from target event

This commit is contained in:
2026-07-18 16:54:26 +01:00
commit e08aa0a667
71 changed files with 15963 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
import { NextResponse } from 'next/server';
import fs from 'fs-extra';
import path from 'path';
import { execFile } from 'child_process';
import util from 'util';
import { getFlacMetadata, parseLabel } from '@/lib/utils/audio';
import { AnalysisTrack, AnalysisResponse } from '@/types';
const execFileAsync = util.promisify(execFile);
// This route analyzes the album: extracts metadata and checks quality
export async function POST(request: Request) {
try {
const { albumPath } = await request.json();
if (!albumPath || !(await fs.pathExists(albumPath))) {
return NextResponse.json({ error: 'Invalid album path' }, { status: 400 });
}
const files = await fs.readdir(albumPath);
const flacFiles = files.filter((file) => file.toLowerCase().endsWith('.flac'));
if (flacFiles.length === 0) {
return NextResponse.json({ error: 'No FLAC files found' }, { status: 400 });
}
const tracks: (AnalysisTrack & { size?: number })[] = await Promise.all(
flacFiles.map(async (file) => {
const filePath = path.join(albumPath, file);
try {
// Check for audio corruption
try {
await execFileAsync('ffmpeg', ['-v', 'error', '-i', filePath, '-f', 'null', '-']);
} catch (ffmpegError) {
return {
filename: file,
path: filePath,
error: 'Corrupted audio data or length metadata mismatch',
};
}
const stats = await fs.stat(filePath);
const metadata = await getFlacMetadata(filePath);
return {
filename: file,
path: filePath,
...metadata,
fullPathLength: filePath.length,
size: stats.size,
};
} catch (e) {
return {
filename: file,
path: filePath,
error: 'Failed to parse metadata',
};
}
})
);
// Calculate total size
const totalSize = tracks.reduce((acc, track) => acc + (track.size || 0), 0);
// Sort tracks by track number if available, else by filename
tracks.sort((a, b) => {
if ('error' in a || 'error' in b) return 0; // Keep order if error
const trackA = a.common?.track?.no ?? 0;
const trackB = b.common?.track?.no ?? 0;
if (trackA && trackB) return trackA - trackB;
return a.filename.localeCompare(b.filename);
});
// Analyze quality
let isLossless = true;
let isHiRes = true;
let mixed = false;
let qualityDetails = '';
const sampleRates = new Set();
const bitDepths = new Set();
tracks.forEach((track) => {
if ('error' in track) return;
const sr = track.format?.sampleRate;
const bd = track.format?.bitsPerSample;
if (sr) sampleRates.add(sr);
if (bd) bitDepths.add(bd);
if (sr !== 44100 || bd !== 16) {
isLossless = false; // Not strictly CD quality
}
if (!sr || sr < 48000 || !bd || bd < 24) {
isHiRes = false; // Not strictly Hi-Res
}
});
if (sampleRates.size > 1 || bitDepths.size > 1) {
mixed = true;
qualityDetails = `NOTE: Mixed formats. Sample Rates: ${Array.from(sampleRates).join(', ')} Hz. Bit Depths: ${Array.from(bitDepths).join(', ')} bit.`;
// Generate detailed message for mixed content
const mixedDetails = tracks.map(t => {
if ('error' in t) return `${t.filename}: Error`;
return `Track ${t.common?.track?.no || '?'}: ${t.format?.bitsPerSample}bit / ${t.format?.sampleRate}Hz`;
}).join('\n');
qualityDetails += '\n' + mixedDetails;
}
// Determine overall status
let status = 'UNKNOWN';
if (tracks.some(t => 'error' in t)) {
status = 'CORRUPT';
} else if (mixed) {
status = 'MIXED';
} else if (isHiRes) {
status = 'HI-RES'; // 24bit LOSSLESS
} else if (sampleRates.has(44100) && bitDepths.has(16) && sampleRates.size === 1 && bitDepths.size === 1) {
status = 'LOSSLESS';
} else {
// Could be 48/16 or something else
status = `OTHER (${Array.from(bitDepths)[0]}bit / ${Array.from(sampleRates)[0]}Hz)`;
}
// Extract Album Metadata (from first track)
const firstValidTrack = tracks.find((t): t is Extract<AnalysisTrack, { error?: never }> => !('error' in t));
let label = parseLabel(firstValidTrack?.common?.copyright || 'Unknown Label');
if (!label) label = 'Unknown Label'; // Fallback if parsing resulted in empty string (e.g. Records DK)
// Additional artists: filter out album artist from artists list
const albumArtist = firstValidTrack?.common?.albumartist || firstValidTrack?.common?.artist || '';
// Collect artists from ALL tracks, not just the first one
const allArtistsSet = new Set<string>();
tracks.forEach(t => {
if ('error' in t) return;
const trackArtists = t.common?.artists || [];
trackArtists.forEach(artist => {
// Split by common delimiters to handle combined strings like "A, B & C"
const parts = artist.split(/,\s+|&|\s+\/\s+/).map(a => a.trim()).filter(a => a.length > 0);
parts.forEach(p => allArtistsSet.add(p));
});
});
const allArtists = Array.from(allArtistsSet);
// Identify all main artists (Album Artist + Track Artist) individually
const mainArtistNames = new Set(
[albumArtist, firstValidTrack?.common?.artist || '']
.flatMap(name => name.split(/,\s+|&|\s+\/\s+/)) // Split by comma, &, /
.map(a => a.trim())
.filter(a => a.length > 0)
);
const additionalArtists = allArtists.filter(a => !mainArtistNames.has(a));
const albumMetadata = {
artist: firstValidTrack?.common?.artist || 'Unknown Artist',
album: firstValidTrack?.common?.album || 'Unknown Album',
year: firstValidTrack?.common?.year || (firstValidTrack?.common?.date ? parseInt(firstValidTrack.common.date.split('-')[0]) : undefined),
upc: firstValidTrack?.common?.barcode,
label: label,
tidalUrl: firstValidTrack?.native?.tidalUrl,
additionalArtists: additionalArtists.length > 0 ? additionalArtists : undefined,
releaseType: (() => {
const name = firstValidTrack?.common?.album?.toLowerCase() || '';
if (name.includes('live')) return 11; // Live album
if (name.includes('soundtrack') || name.includes('ost')) return 3; // Soundtrack
const validTracks = tracks.filter((t) => !('error' in t)).length;
if (validTracks === 1) return 9; // Single
if (validTracks <= 5) return 5; // EP
return 1; // Album
})(),
totalSize
};
return NextResponse.json({
albumPath,
metadata: albumMetadata,
tracks,
quality: {
status,
mixed,
details: qualityDetails,
},
});
} catch (error) {
console.error('Error analyzing album:', error);
return NextResponse.json({ error: 'Failed to analyze album' }, { status: 500 });
}
}