Files
crossfade/app/api/red-template/route.ts
T

260 lines
9.7 KiB
TypeScript

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<AnalysisTrack, { error?: never }> => !('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<string, number>();
const allUniqueArtists = new Set<string>();
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 });
}
}