diff --git a/packages/backend/src/routes/songs.ts b/packages/backend/src/routes/songs.ts index 464cc9a..34fde89 100644 --- a/packages/backend/src/routes/songs.ts +++ b/packages/backend/src/routes/songs.ts @@ -195,34 +195,28 @@ router.get('/count', async (req: Request, res: Response) => { } }); -// Export library to XML format +// Export library to XML format with streaming 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); + console.log('Starting streaming XML export...'); // 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"`); + res.setHeader('Transfer-Encoding', 'chunked'); - console.log('XML export completed successfully'); - res.send(xmlContent); + // Import the streaming XML generation function + const { streamToXml } = await import('../services/xmlService.js'); + + // Stream XML generation to response + await streamToXml(res); + + console.log('Streaming XML export completed successfully'); } catch (error) { console.error('Error exporting library:', error); - res.status(500).json({ message: 'Error exporting library', error }); + if (!res.headersSent) { + res.status(500).json({ message: 'Error exporting library', error }); + } } }); diff --git a/packages/backend/src/services/xmlService.ts b/packages/backend/src/services/xmlService.ts index d985cc8..5a13228 100644 --- a/packages/backend/src/services/xmlService.ts +++ b/packages/backend/src/services/xmlService.ts @@ -29,6 +29,97 @@ const buildXmlNode = (node: any): any => { return xmlNode; }; +export const streamToXml = async (res: any) => { + // Write XML header + res.write('\n'); + res.write('\n'); + + // Start COLLECTION section + const songCount = await Song.countDocuments(); + res.write(` \n`); + + // Stream songs in batches to avoid memory issues + const batchSize = 100; + let processedSongs = 0; + + while (processedSongs < songCount) { + const songs = await Song.find({}) + .skip(processedSongs) + .limit(batchSize) + .lean(); + + for (const song of songs) { + res.write(` \n \n \n`); + } else { + res.write('/>\n'); + } + } + + processedSongs += songs.length; + console.log(`Streamed ${processedSongs}/${songCount} songs...`); + } + + res.write(' \n'); + + // Start PLAYLISTS section + res.write(' \n'); + res.write(' \n'); + + // Stream playlists + const playlists = await Playlist.find({}).lean(); + for (const playlist of playlists) { + await streamPlaylistNode(res, playlist, 6); + } + + res.write(' \n'); + res.write(' \n'); + res.write(''); + + res.end(); +}; + +const streamPlaylistNode = async (res: any, node: any, indent: number) => { + const spaces = ' '.repeat(indent); + const nodeType = node.type === 'folder' ? '0' : '1'; + + if (node.type === 'folder') { + const childCount = node.children ? node.children.length : 0; + res.write(`${spaces}\n`); + + if (node.children && node.children.length > 0) { + for (const child of node.children) { + await streamPlaylistNode(res, child, indent + 2); + } + } + + res.write(`${spaces}\n`); + } else { + const trackCount = node.tracks ? node.tracks.length : 0; + res.write(`${spaces}\n`); + + if (node.tracks && node.tracks.length > 0) { + for (const trackId of node.tracks) { + res.write(`${spaces} \n`); + } + } + + res.write(`${spaces}\n`); + } +}; + +const escapeXml = (text: string): string => { + if (!text) return ''; + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +}; + export const exportToXml = (songs: any[], playlists: any[]): string => { const xml = create({ DJ_PLAYLISTS: {