import { NextResponse } from 'next/server'; import fs from 'fs-extra'; import path from 'path'; import { getFlacMetadata, parseLabel } from '@/lib/utils/audio'; import { AnalysisTrack, ApiResponse, AlbumResponse } from '@/types'; export async function POST(request: Request) { try { const { albumPath, releaseType } = 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[] = await Promise.all( flacFiles.map(async (file) => { const filePath = path.join(albumPath, file); const metadata = await getFlacMetadata(filePath); return { filename: file, path: filePath, ...metadata, }; }) ); // Sort tracks tracks.sort((a, b) => { if ('error' in a || 'error' in b) return 0; const discA = a.common?.disk?.no ?? 1; const discB = b.common?.disk?.no ?? 1; if (discA !== discB) return discA - discB; 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 for description 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; if (!sr || sr < 48000 || !bd || bd < 24) isHiRes = false; }); const discNumbers = new Set( tracks.map(t => ('error' in t ? 1 : (t.common?.disk?.no || 1))) ); const isMultiDisc = discNumbers.size > 1 || Array.from(discNumbers).some(d => d > 1); if (sampleRates.size > 1 || bitDepths.size > 1) { mixed = true; qualityDetails = `\n\nNOTE: Mixed formats. Sample Rates: ${Array.from(sampleRates).join(', ')} Hz. Bit Depths: ${Array.from(bitDepths).join(', ')} bit.`; const mixedDetails = tracks.map(t => { if ('error' in t) return `${t.filename}: Error`; const discNo = t.common?.disk?.no || 1; const trackNo = t.common?.track?.no; let trackStr = trackNo !== undefined && trackNo !== null ? String(trackNo).padStart(2, '0') : '?'; if (isMultiDisc) { trackStr = `${discNo}-${trackStr}`; } return `Track ${trackStr}: ${t.format?.bitsPerSample}bit / ${t.format?.sampleRate}Hz`; }).join('\n'); qualityDetails += '\n' + mixedDetails; } const firstTrack = tracks.find((t): t is Extract => !('error' in t)); if (!firstTrack) { return NextResponse.json({ error: 'No valid tracks found' }, { status: 400 }); } const albumName = firstTrack.common?.album || 'Unknown Album'; const year = firstTrack.common?.year || (firstTrack.common?.date ? parseInt(firstTrack.common.date.split('-')[0]) : 2025); let label = parseLabel(firstTrack.common?.copyright || ''); const upc = firstTrack.native?.vorbis?.find(tag => tag.id === 'UPC')?.value || firstTrack.common?.barcode || ''; // Artists // Artists Logic // Helper to split artists const splitArtists = (artistString: string) => { return artistString.split(/,\s+|\s+\/\s+|;\s+/).map((a: string) => a.trim()).filter((a: string) => a.length > 0); }; // Collect all artists and their frequencies const artistCounts = new Map(); const allUniqueArtists = new Set(); tracks.forEach(t => { if ('error' in t) return; let trackArtists: string[] = []; // music-metadata might return artists as an array, but sometimes elements are comma-separated strings if (t.common?.artists) { t.common.artists.forEach((a: string) => trackArtists.push(...splitArtists(a))); } else if (t.common?.artist) { trackArtists = splitArtists(t.common.artist); } trackArtists.forEach(a => { allUniqueArtists.add(a); artistCounts.set(a, (artistCounts.get(a) || 0) + 1); }); }); let mainArtists: { name: string }[] = []; const withArtists: { name: string }[] = []; if (releaseType === 7) { // Compilation: All artists in mainArtists, no withArtists allUniqueArtists.forEach(a => { if (a !== 'Various Artists') { mainArtists.push({ name: a }); } }); } else { // Standard Logic // Determine Main Artist String let mainArtistName = firstTrack.common?.albumartist || 'Various Artists'; // If album artist is Various Artists (or missing), find the most common artist if (mainArtistName === 'Various Artists' || !mainArtistName) { let maxCount = 0; let mostFrequent = ''; artistCounts.forEach((count, artist) => { if (count > maxCount) { maxCount = count; mostFrequent = artist; } }); if (mostFrequent) { mainArtistName = mostFrequent; } } // Split main artist into components for the array // Use the same split logic as above const mainArtistComponents = splitArtists(mainArtistName); mainArtists = mainArtistComponents.map(name => ({ name })); // Determine With Artists allUniqueArtists.forEach(a => { // Exclude all main artist components and "Various Artists" from "with" list if (!mainArtistComponents.includes(a) && a !== 'Various Artists') { withArtists.push({ name: a }); } }); } // Tracklist let currentDisc: number | null = null; const tracklistLines: string[] = []; tracks.forEach(t => { if ('error' in t) { tracklistLines.push(`${t.filename} (Error)`); return; } const discNo = t.common?.disk?.no || 1; const trackNo = t.common?.track?.no ? String(t.common.track.no).padStart(2, '0') : '00'; if (isMultiDisc && discNo !== currentDisc) { if (currentDisc !== null) tracklistLines.push(''); tracklistLines.push(`[b]Disc ${discNo}[/b]`); currentDisc = discNo; } const trackLabel = isMultiDisc ? `${discNo}-${trackNo}` : trackNo; tracklistLines.push(`${trackLabel}. ${t.common?.title || t.filename}`); }); const tracklist = tracklistLines.join('\n'); // Description let description = "Sourced from TIDAL"; if (mixed) { description += qualityDetails; } // Encoding let encoding = "Lossless"; const maxBitDepth = Math.max(...Array.from(bitDepths).map(Number)); if (maxBitDepth >= 24) { encoding = "24bit Lossless"; } const response = { status: "success", response: { source: "Crossfade", group: { wikiBody: tracklist, bbBody: tracklist, name: albumName, year: year, categoryId: 1, releaseType: releaseType || 1, // Default to Album (1) categoryName: "Music", musicInfo: { composers: [], dj: [], artists: mainArtists, with: withArtists, conductor: [], remixedBy: [], producer: [] }, recordLabel: label, catalogueNumber: upc, tags: [] // Empty for now, hard to guess }, torrent: { media: "WEB", format: "FLAC", encoding: encoding, //remastered: true, remasterYear: year, remasterRecordLabel: label, remasterCatalogueNumber: upc, description: description, //fileCount: tracks.length, //filePath: `${mainArtistName} - ${albumName} (${year}) [FLAC]` } } }; return NextResponse.json(response); } catch (error) { console.error('Error generating RED template:', error); return NextResponse.json({ error: 'Failed to generate template' }, { status: 500 }); } }