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
|
// Create multiple songs
|
||||||
router.post('/batch', async (req: Request, res: Response) => {
|
router.post('/batch', async (req: Request, res: Response) => {
|
||||||
try {
|
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 { 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(() => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user