feat(export): honor custom playlist order overlay when exporting Rekordbox XML (Entries + TRACK sequence)

This commit is contained in:
Geert Rademakes 2025-08-08 14:56:28 +02:00
parent 9249a5a4a7
commit 5659dde540

View File

@ -2,6 +2,17 @@ import { create } from "xmlbuilder";
import { Song } from "../models/Song.js";
import { Playlist } from "../models/Playlist.js";
// Compute effective playlist order: custom overlay (order) + remaining base tracks
const getEffectiveTrackIds = (node: any): string[] => {
const base: string[] = Array.isArray(node?.tracks) ? node.tracks : [];
const overlay: string[] = Array.isArray(node?.order) ? node.order : [];
if (overlay.length === 0) return base;
const setBase = new Set(base);
const orderedKnown = overlay.filter((id: string) => setBase.has(id));
const missing = base.filter((id: string) => !overlay.includes(id));
return [...orderedKnown, ...missing];
};
const buildXmlNode = (node: any): any => {
const xmlNode: any = {
'@Type': node.type === 'folder' ? '0' : '1',
@ -14,13 +25,12 @@ const buildXmlNode = (node: any): any => {
xmlNode.NODE = node.children.map((child: any) => buildXmlNode(child));
}
} else {
// For playlists, always include KeyType and Entries
// For playlists, include KeyType and Entries based on effective order
const effectiveIds = getEffectiveTrackIds(node);
xmlNode['@KeyType'] = '0';
// Set Entries to the actual number of tracks
xmlNode['@Entries'] = (node.tracks || []).length;
// Include TRACK elements if there are tracks
if (node.tracks && node.tracks.length > 0) {
xmlNode.TRACK = node.tracks.map((trackId: string) => ({
xmlNode['@Entries'] = effectiveIds.length;
if (effectiveIds.length > 0) {
xmlNode.TRACK = effectiveIds.map((trackId: string) => ({
'@Key': trackId
}));
}
@ -110,15 +120,13 @@ const streamPlaylistNodeFull = async (res: any, node: any) => {
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) {
const effectiveIds = getEffectiveTrackIds(node);
res.write(`\n <NODE Name="${escapeXml(node.name)}" Type="${nodeType}" KeyType="0" Entries="${effectiveIds.length}">`);
if (effectiveIds.length > 0) {
for (const trackId of effectiveIds) {
res.write(`\n <TRACK Key="${trackId}"/>`);
}
}
res.write('\n </NODE>');
}
};
@ -138,15 +146,13 @@ const streamPlaylistNodeCompact = async (res: any, node: any) => {
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) {
const effectiveIds = getEffectiveTrackIds(node);
res.write(`<NODE Name="${escapeXml(node.name)}" Type="${nodeType}" Count="${effectiveIds.length}">`);
if (effectiveIds.length > 0) {
for (const trackId of effectiveIds) {
res.write(`<TRACK Key="${trackId}"/>`);
}
}
res.write('</NODE>');
}
};
@ -167,15 +173,13 @@ const streamPlaylistNode = async (res: any, node: any, indent: number) => {
res.write(`${spaces}</NODE>\n`);
} else {
const trackCount = node.tracks ? node.tracks.length : 0;
res.write(`${spaces}<NODE Type="${nodeType}" Name="${escapeXml(node.name)}" KeyType="0" Entries="${trackCount}">\n`);
if (node.tracks && node.tracks.length > 0) {
for (const trackId of node.tracks) {
const effectiveIds = getEffectiveTrackIds(node);
res.write(`${spaces}<NODE Type="${nodeType}" Name="${escapeXml(node.name)}" KeyType="0" Entries="${effectiveIds.length}">\n`);
if (effectiveIds.length > 0) {
for (const trackId of effectiveIds) {
res.write(`${spaces} <TRACK Key="${trackId}"/>\n`);
}
}
res.write(`${spaces}</NODE>\n`);
}
};