feat: Move export functionality to backend for complete database export - Create backend XML service for generating Rekordbox-compatible XML - Add /api/songs/export endpoint that exports entire database - Frontend now calls backend API instead of local function - Exports ALL songs and playlists from database, not just loaded ones - Proper file download with correct headers and filename

This commit is contained in:
Geert Rademakes 2025-08-06 11:26:51 +02:00
parent 1cba4e0eeb
commit 7286140bd5
3 changed files with 125 additions and 5 deletions

View File

@ -195,6 +195,37 @@ router.get('/count', async (req: Request, res: Response) => {
} }
}); });
// Export library to XML format
router.get('/export', async (req: Request, res: Response) => {
try {
console.log('Exporting library to XML format...');
// Get all songs from database
const allSongs = await Song.find({}).lean();
console.log(`Found ${allSongs.length} songs to export`);
// Get all playlists from database
const allPlaylists = await Playlist.find({}).lean();
console.log(`Found ${allPlaylists.length} playlists to export`);
// Import the XML generation function
const { exportToXml } = await import('../services/xmlService.js');
// Generate XML content
const xmlContent = exportToXml(allSongs, allPlaylists);
// Set response headers for file download
res.setHeader('Content-Type', 'application/xml');
res.setHeader('Content-Disposition', `attachment; filename="rekordbox-library-${new Date().toISOString().split('T')[0]}.xml"`);
console.log('XML export completed successfully');
res.send(xmlContent);
} catch (error) {
console.error('Error exporting library:', error);
res.status(500).json({ message: 'Error exporting library', error });
}
});
// Create multiple songs // Create multiple songs
router.post('/batch', async (req: Request, res: Response) => { router.post('/batch', async (req: Request, res: Response) => {
try { try {

View File

@ -0,0 +1,86 @@
import { create } from "xmlbuilder";
import { Song } from "../models/Song.js";
import { Playlist } from "../models/Playlist.js";
const buildXmlNode = (node: any): any => {
const xmlNode: any = {
'@Type': node.type === 'folder' ? '0' : '1',
'@Name': node.name,
};
if (node.type === 'folder') {
xmlNode['@Count'] = (node.children || []).length;
if (node.children && node.children.length > 0) {
xmlNode.NODE = node.children.map((child: any) => buildXmlNode(child));
}
} else {
// For playlists, always include KeyType and Entries
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) => ({
'@Key': trackId
}));
}
}
return xmlNode;
};
export const exportToXml = (songs: any[], playlists: any[]): string => {
const xml = create({
DJ_PLAYLISTS: {
'@Version': '1.0.0',
COLLECTION: {
'@Entries': songs.length,
TRACK: songs.map(song => ({
'@TrackID': song.id,
'@Name': song.title,
'@Artist': song.artist,
'@Composer': song.composer,
'@Album': song.album,
'@Grouping': song.grouping,
'@Genre': song.genre,
'@Kind': 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': song.comments,
'@PlayCount': song.playCount,
'@Rating': song.rating,
'@Location': song.location,
'@Remixer': song.remixer,
'@Tonality': song.tonality,
'@Label': song.label,
'@Mix': song.mix,
...(song.tempo ? {
TEMPO: {
'@Inizio': song.tempo.inizio,
'@Bpm': song.tempo.bpm,
'@Metro': song.tempo.metro,
'@Battito': song.tempo.battito
}
} : {})
}))
},
PLAYLISTS: {
NODE: {
'@Type': '0',
'@Name': 'ROOT',
'@Count': playlists.length,
NODE: playlists.map(playlist => buildXmlNode(playlist))
}
}
}
});
return xml.end({ pretty: true });
};

View File

@ -9,7 +9,6 @@ import { Configuration } from "./pages/Configuration";
import { useXmlParser } from "./hooks/useXmlParser"; import { useXmlParser } from "./hooks/useXmlParser";
import { usePaginatedSongs } from "./hooks/usePaginatedSongs"; import { usePaginatedSongs } from "./hooks/usePaginatedSongs";
import { formatTotalDuration } from "./utils/formatters"; import { formatTotalDuration } from "./utils/formatters";
import { exportToXml } from "./services/xmlService";
import { api } from "./services/api"; import { api } from "./services/api";
import type { Song, PlaylistNode } from "./types/interfaces"; import type { Song, PlaylistNode } from "./types/interfaces";
@ -116,10 +115,14 @@ export default function RekordboxReader() {
} = usePaginatedSongs({ pageSize: 50, playlistName: currentPlaylist }); } = usePaginatedSongs({ pageSize: 50, playlistName: currentPlaylist });
// Export library to XML // Export library to XML
const handleExportLibrary = useCallback(() => { const handleExportLibrary = useCallback(async () => {
try { try {
const xmlContent = exportToXml(songs, playlists); const response = await fetch('/api/songs/export');
const blob = new Blob([xmlContent], { type: 'application/xml' }); if (!response.ok) {
throw new Error('Failed to export library');
}
const blob = await response.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@ -131,7 +134,7 @@ export default function RekordboxReader() {
} catch (error) { } catch (error) {
console.error('Failed to export library:', error); console.error('Failed to export library:', error);
} }
}, [songs, playlists]); }, []);
// Check if database is initialized (has songs or playlists) - moved after useDisclosure // Check if database is initialized (has songs or playlists) - moved after useDisclosure
useEffect(() => { useEffect(() => {