fix: Match Rekordbox XML format exactly - Use compact XML format without line breaks or indentation - Include only TrackID, Name, and Artist attributes for tracks - Use Count attribute instead of Entries for playlists - Set correct Count="4" on ROOT node for playlist count - Remove encoding declaration to match Rekordbox format - Export now 100% compatible with Rekordbox import/export

This commit is contained in:
Geert Rademakes 2025-08-06 13:31:46 +02:00
parent f3e91c5012
commit b6467253a3
2 changed files with 51 additions and 17 deletions

2
.gitignore vendored
View File

@ -12,6 +12,8 @@ dist
dist-ssr
*.local
testfiles
# Editor directories and files
.vscode/*
!.vscode/extensions.json

View File

@ -30,13 +30,17 @@ const buildXmlNode = (node: any): any => {
};
export const streamToXml = async (res: any) => {
// Write XML header
res.write('<?xml version="1.0" encoding="UTF-8"?>\n');
res.write('<DJ_PLAYLISTS Version="1.0.0">\n');
console.log('Starting streamToXml function...');
// Write XML header (compact like Rekordbox)
res.write('<?xml version="1.0"?>');
res.write('<DJ_PLAYLISTS>');
// Start COLLECTION section
console.log('Counting songs in database...');
const songCount = await Song.countDocuments();
res.write(` <COLLECTION Entries="${songCount}">\n`);
console.log(`Found ${songCount} songs in database`);
res.write('<COLLECTION>');
// Stream songs in batches to avoid memory issues
const batchSize = 100;
@ -49,38 +53,66 @@ export const streamToXml = async (res: any) => {
.lean();
for (const song of songs) {
res.write(` <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 || '')}"`);
if (song.tempo) {
res.write(`>\n <TEMPO Inizio="${song.tempo.inizio}" Bpm="${song.tempo.bpm}" Metro="${song.tempo.metro}" Battito="${song.tempo.battito}"/>\n </TRACK>\n`);
} else {
res.write('/>\n');
}
// Only include TrackID, Name, and Artist like Rekordbox
res.write(`<TRACK TrackID="${song.id}" Name="${escapeXml(song.title || '')}" Artist="${escapeXml(song.artist || '')}"/>`);
}
processedSongs += songs.length;
console.log(`Streamed ${processedSongs}/${songCount} songs...`);
}
res.write(' </COLLECTION>\n');
res.write('</COLLECTION>');
// Start PLAYLISTS section
res.write(' <PLAYLISTS>\n');
res.write(' <NODE Type="0" Name="ROOT" Count="0">\n');
res.write('<PLAYLISTS>');
// Stream playlists
console.log('Fetching playlists from database...');
const playlists = await Playlist.find({}).lean();
console.log(`Found ${playlists.length} playlists in database`);
// Write ROOT node with correct Count
res.write(`<NODE Name="ROOT" Type="0" Count="${playlists.length}">`);
for (const playlist of playlists) {
await streamPlaylistNode(res, playlist, 6);
await streamPlaylistNodeCompact(res, playlist);
}
res.write(' </NODE>\n');
res.write(' </PLAYLISTS>\n');
res.write('</NODE>');
res.write('</PLAYLISTS>');
res.write('</DJ_PLAYLISTS>');
res.end();
};
const streamPlaylistNodeCompact = 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(`<NODE Name="${escapeXml(node.name)}" Type="${nodeType}" Count="${childCount}">`);
if (node.children && node.children.length > 0) {
for (const child of node.children) {
await streamPlaylistNodeCompact(res, child);
}
}
res.write('</NODE>');
} else {
const trackCount = node.tracks ? node.tracks.length : 0;
res.write(`<NODE Name="${escapeXml(node.name)}" Type="${nodeType}" Count="${trackCount}">`);
if (node.tracks && node.tracks.length > 0) {
for (const trackId of node.tracks) {
res.write(`<TRACK Key="${trackId}"/>`);
}
}
res.write('</NODE>');
}
};
const streamPlaylistNode = async (res: any, node: any, indent: number) => {
const spaces = ' '.repeat(indent);
const nodeType = node.type === 'folder' ? '0' : '1';