feat: Support full Rekordbox master collection format - Include all track properties (Composer, Album, Genre, Size, TotalTime, etc.) - Add PRODUCT section with rekordbox version info - Include TEMPO entries for beat grid information - Use proper indentation and formatting like master collection - Support KeyType and Entries attributes for playlists - Handle large libraries (7000+ songs) with streaming export - Full compatibility with Rekordbox master collection format

This commit is contained in:
Geert Rademakes 2025-08-06 13:34:08 +02:00
parent b6467253a3
commit 1450eaa29b

View File

@ -32,15 +32,18 @@ const buildXmlNode = (node: any): any => {
export const streamToXml = async (res: any) => { export const streamToXml = async (res: any) => {
console.log('Starting streamToXml function...'); console.log('Starting streamToXml function...');
// Write XML header (compact like Rekordbox) // Write XML header with encoding (like master collection)
res.write('<?xml version="1.0"?>'); res.write('<?xml version="1.0" encoding="UTF-8"?>');
res.write('<DJ_PLAYLISTS>'); res.write('\n\n<DJ_PLAYLISTS Version="1.0.0">');
// Add PRODUCT section
res.write('\n <PRODUCT Name="rekordbox" Version="7.1.3" Company="AlphaTheta"/>');
// Start COLLECTION section // Start COLLECTION section
console.log('Counting songs in database...'); console.log('Counting songs in database...');
const songCount = await Song.countDocuments(); const songCount = await Song.countDocuments();
console.log(`Found ${songCount} songs in database`); console.log(`Found ${songCount} songs in database`);
res.write('<COLLECTION>'); res.write(`\n <COLLECTION Entries="${songCount}">`);
// Stream songs in batches to avoid memory issues // Stream songs in batches to avoid memory issues
const batchSize = 100; const batchSize = 100;
@ -53,18 +56,25 @@ export const streamToXml = async (res: any) => {
.lean(); .lean();
for (const song of songs) { for (const song of songs) {
// Only include TrackID, Name, and Artist like Rekordbox // Include ALL track attributes like master collection
res.write(`<TRACK TrackID="${song.id}" Name="${escapeXml(song.title || '')}" Artist="${escapeXml(song.artist || '')}"/>`); res.write(`\n <TRACK TrackID="${song.id}" Name="${escapeXml(song.title || '')}" Artist="${escapeXml(song.artist || '')}" Composer="${escapeXml(song.composer || '')}" Album="${escapeXml(song.album || '')}" Grouping="${escapeXml(song.grouping || '')}" Genre="${escapeXml(song.genre || '')}" Kind="${escapeXml(song.kind || '')}" Size="${song.size || ''}" TotalTime="${song.totalTime || ''}" DiscNumber="${song.discNumber || ''}" TrackNumber="${song.trackNumber || ''}" Year="${song.year || ''}" AverageBpm="${song.averageBpm || ''}" DateAdded="${song.dateAdded || ''}" BitRate="${song.bitRate || ''}" SampleRate="${song.sampleRate || ''}" Comments="${escapeXml(song.comments || '')}" PlayCount="${song.playCount || ''}" Rating="${song.rating || ''}" Location="${escapeXml(song.location || '')}" Remixer="${escapeXml(song.remixer || '')}" Tonality="${escapeXml(song.tonality || '')}" Label="${escapeXml(song.label || '')}" Mix="${escapeXml(song.mix || '')}">`);
// Add TEMPO entries if they exist
if (song.tempo) {
res.write(`\n <TEMPO Inizio="${song.tempo.inizio}" Bpm="${song.tempo.bpm}" Metro="${song.tempo.metro}" Battito="${song.tempo.battito}"/>`);
}
res.write('\n </TRACK>');
} }
processedSongs += songs.length; processedSongs += songs.length;
console.log(`Streamed ${processedSongs}/${songCount} songs...`); console.log(`Streamed ${processedSongs}/${songCount} songs...`);
} }
res.write('</COLLECTION>'); res.write('\n </COLLECTION>');
// Start PLAYLISTS section // Start PLAYLISTS section
res.write('<PLAYLISTS>'); res.write('\n <PLAYLISTS>');
// Stream playlists // Stream playlists
console.log('Fetching playlists from database...'); console.log('Fetching playlists from database...');
@ -72,19 +82,47 @@ export const streamToXml = async (res: any) => {
console.log(`Found ${playlists.length} playlists in database`); console.log(`Found ${playlists.length} playlists in database`);
// Write ROOT node with correct Count // Write ROOT node with correct Count
res.write(`<NODE Name="ROOT" Type="0" Count="${playlists.length}">`); res.write(`\n <NODE Type="0" Name="ROOT" Count="${playlists.length}">`);
for (const playlist of playlists) { for (const playlist of playlists) {
await streamPlaylistNodeCompact(res, playlist); await streamPlaylistNodeFull(res, playlist);
} }
res.write('</NODE>'); res.write('\n </NODE>');
res.write('</PLAYLISTS>'); res.write('\n </PLAYLISTS>');
res.write('</DJ_PLAYLISTS>'); res.write('\n</DJ_PLAYLISTS>');
res.end(); res.end();
}; };
const streamPlaylistNodeFull = async (res: any, node: any) => {
const nodeType = node.type === 'folder' ? '0' : '1';
if (node.type === 'folder') {
const childCount = node.children ? node.children.length : 0;
res.write(`\n <NODE Name="${escapeXml(node.name)}" Type="${nodeType}" Count="${childCount}">`);
if (node.children && node.children.length > 0) {
for (const child of node.children) {
await streamPlaylistNodeFull(res, child);
}
}
res.write('\n </NODE>');
} else {
const trackCount = node.tracks ? node.tracks.length : 0;
res.write(`\n <NODE Name="${escapeXml(node.name)}" Type="${nodeType}" KeyType="0" Entries="${trackCount}">`);
if (node.tracks && node.tracks.length > 0) {
for (const trackId of node.tracks) {
res.write(`\n <TRACK Key="${trackId}"/>`);
}
}
res.write('\n </NODE>');
}
};
const streamPlaylistNodeCompact = async (res: any, node: any) => { const streamPlaylistNodeCompact = async (res: any, node: any) => {
const nodeType = node.type === 'folder' ? '0' : '1'; const nodeType = node.type === 'folder' ? '0' : '1';