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:
parent
1cba4e0eeb
commit
7286140bd5
@ -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
|
||||
router.post('/batch', async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
86
packages/backend/src/services/xmlService.ts
Normal file
86
packages/backend/src/services/xmlService.ts
Normal 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 });
|
||||
};
|
||||
@ -9,7 +9,6 @@ import { Configuration } from "./pages/Configuration";
|
||||
import { useXmlParser } from "./hooks/useXmlParser";
|
||||
import { usePaginatedSongs } from "./hooks/usePaginatedSongs";
|
||||
import { formatTotalDuration } from "./utils/formatters";
|
||||
import { exportToXml } from "./services/xmlService";
|
||||
|
||||
import { api } from "./services/api";
|
||||
import type { Song, PlaylistNode } from "./types/interfaces";
|
||||
@ -116,10 +115,14 @@ export default function RekordboxReader() {
|
||||
} = usePaginatedSongs({ pageSize: 50, playlistName: currentPlaylist });
|
||||
|
||||
// Export library to XML
|
||||
const handleExportLibrary = useCallback(() => {
|
||||
const handleExportLibrary = useCallback(async () => {
|
||||
try {
|
||||
const xmlContent = exportToXml(songs, playlists);
|
||||
const blob = new Blob([xmlContent], { type: 'application/xml' });
|
||||
const response = await fetch('/api/songs/export');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to export library');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@ -131,7 +134,7 @@ export default function RekordboxReader() {
|
||||
} catch (error) {
|
||||
console.error('Failed to export library:', error);
|
||||
}
|
||||
}, [songs, playlists]);
|
||||
}, []);
|
||||
|
||||
// Check if database is initialized (has songs or playlists) - moved after useDisclosure
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user