frontend AND backend bro!!

This commit is contained in:
Geert Rademakes 2025-04-24 15:28:22 +02:00
parent 32823fd40d
commit 901c78990b
32 changed files with 7413 additions and 1062 deletions

2113
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,18 @@
{ {
"name": "rekordbox-reader", "name": "rekordbox-reader-monorepo",
"private": true, "private": true,
"version": "0.0.0", "version": "1.0.0",
"type": "module", "workspaces": [
"packages/*"
],
"scripts": { "scripts": {
"dev": "vite", "dev:frontend": "cd packages/frontend && npm run dev",
"build": "tsc -b && vite build", "dev:backend": "cd packages/backend && npm run dev",
"lint": "eslint .", "dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\"",
"preview": "vite preview" "build": "npm run build --workspaces",
}, "install:all": "npm install && npm install --workspaces"
"dependencies": {
"@chakra-ui/modal": "^2.3.1",
"@chakra-ui/react": "^3.12.0",
"@chakra-ui/transition": "^2.1.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"events": "^3.3.0",
"framer-motion": "^12.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sax": "^1.4.1",
"xml2js": "^0.6.2",
"xmlbuilder": "^15.1.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "concurrently": "^8.2.2"
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/xml2js": "^0.4.14",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.13",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
} }
} }

2
packages/backend/.env Normal file
View File

@ -0,0 +1,2 @@
PORT=3000
MONGODB_URI=mongodb://localhost:27017/rekordbox

View File

@ -0,0 +1,24 @@
{
"name": "@rekordbox-reader/backend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"mongoose": "^8.2.1"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.11.24",
"tsx": "^4.7.1",
"typescript": "^5.3.3"
}
}

View File

@ -0,0 +1,27 @@
import express from 'express';
import cors from 'cors';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import { songsRouter } from './routes/songs.js';
import { playlistsRouter } from './routes/playlists.js';
dotenv.config();
const app = express();
const port = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/rekordbox')
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('MongoDB connection error:', err));
// Routes
app.use('/api/songs', songsRouter);
app.use('/api/playlists', playlistsRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

View File

@ -0,0 +1,8 @@
import mongoose from 'mongoose';
const playlistSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
tracks: [{ type: String, ref: 'Song' }],
});
export const Playlist = mongoose.model('Playlist', playlistSchema);

View File

@ -0,0 +1,53 @@
import mongoose from 'mongoose';
const tempoSchema = new mongoose.Schema({
inizio: String,
bpm: String,
metro: String,
battito: String,
}, { _id: false });
const songSchema = new mongoose.Schema({
id: { type: String, required: true, unique: true },
title: { type: String, required: true },
artist: String,
composer: String,
album: String,
grouping: String,
genre: String,
kind: String,
size: String,
totalTime: String,
discNumber: String,
trackNumber: String,
year: String,
averageBpm: String,
dateAdded: String,
bitRate: String,
sampleRate: String,
comments: String,
playCount: String,
rating: String,
location: String,
remixer: String,
tonality: String,
label: String,
mix: String,
tempo: tempoSchema,
}, {
timestamps: true,
versionKey: false,
toJSON: {
transform: function(doc, ret) {
ret.id = ret.id || ret._id;
delete ret._id;
delete ret.__v;
return ret;
}
}
});
// Ensure index on id field
songSchema.index({ id: 1 }, { unique: true });
export const Song = mongoose.model('Song', songSchema);

View File

@ -0,0 +1,30 @@
import express from 'express';
import { Playlist } from '../models/Playlist.js';
const router = express.Router();
// Get all playlists
router.get('/', async (req, res) => {
try {
const playlists = await Playlist.find();
res.json(playlists);
} catch (error) {
res.status(500).json({ message: 'Error fetching playlists', error });
}
});
// Create or update playlists
router.post('/batch', async (req, res) => {
try {
const playlists = req.body;
// Delete all existing playlists first
await Playlist.deleteMany({});
// Insert new playlists
const result = await Playlist.insertMany(playlists);
res.status(201).json(result);
} catch (error) {
res.status(500).json({ message: 'Error creating playlists', error });
}
});
export const playlistsRouter = router;

View File

@ -0,0 +1,40 @@
import express, { Request, Response } from 'express';
import { Song } from '../models/Song.js';
const router = express.Router();
// Get all songs
router.get('/', async (req: Request, res: Response) => {
try {
console.log('Fetching songs from database...');
const songs = await Song.find().lean();
console.log(`Found ${songs.length} songs`);
res.json(songs);
} catch (error) {
console.error('Error fetching songs:', error);
res.status(500).json({ message: 'Error fetching songs', error });
}
});
// Create multiple songs
router.post('/batch', async (req: Request, res: Response) => {
try {
console.log('Received batch upload request');
const songs = req.body;
console.log(`Attempting to save ${songs.length} songs`);
// Delete all existing songs first
await Song.deleteMany({});
console.log('Cleared existing songs');
// Insert new songs
const result = await Song.insertMany(songs);
console.log(`Successfully saved ${result.length} songs`);
res.status(201).json(result);
} catch (error) {
console.error('Error creating songs:', error);
res.status(500).json({ message: 'Error creating songs', error });
}
});
export const songsRouter = router;

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

5249
packages/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "rekordbox-reader",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@chakra-ui/modal": "^2.3.1",
"@chakra-ui/react": "^3.12.0",
"@chakra-ui/transition": "^2.1.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"events": "^3.3.0",
"framer-motion": "^12.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sax": "^1.4.1",
"xml2js": "^0.6.2",
"xmlbuilder": "^15.1.1"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/xml2js": "^0.4.14",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.13",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
}
}

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,91 @@
import { Box, Button, Flex, Heading, Input, Spinner, Text } from "@chakra-ui/react";
import { useState } from "react";
import { SongList } from "./components/SongList";
import { PlaylistManager } from "./components/PlaylistManager";
import { useXmlParser } from "./hooks/useXmlParser";
import { exportToXml } from "./services/xmlService";
import { api } from "./services/api";
import "./App.css";
export default function RekordboxReader() {
const { songs, playlists, setPlaylists, handleFileUpload, loading } = useXmlParser();
const [selectedItem, setSelectedItem] = useState<string | null>("All Songs");
const handleCreatePlaylist = async (name: string) => {
const newPlaylist = { name, tracks: [] };
const updatedPlaylists = [...playlists, newPlaylist];
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
setPlaylists(savedPlaylists);
};
const handleAddSongToPlaylist = async (songId: string, playlistName: string) => {
const updatedPlaylists = playlists.map((playlist) =>
playlist.name === playlistName
? { ...playlist, tracks: [...playlist.tracks, songId] }
: playlist
);
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
setPlaylists(savedPlaylists);
};
const handleExport = () => {
const xmlContent = exportToXml(songs, playlists);
const blob = new Blob([xmlContent], { type: "application/xml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "rekordbox_playlists.xml";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const displayedSongs = selectedItem === "All Songs"
? songs
: songs.filter((song) =>
playlists.find((p) => p.name === selectedItem)?.tracks.includes(song.id)
);
if (loading) {
return (
<Flex height="100vh" align="center" justify="center" direction="column" gap={4}>
<Spinner size="xl" />
<Text>Loading your library...</Text>
</Flex>
);
}
return (
<Box p={5}>
<Heading mb={4}>Rekordbox Reader</Heading>
<Input
type="file"
accept=".xml"
onChange={handleFileUpload}
mb={4}
/>
<Flex gap={4}>
<Box w="250px">
<PlaylistManager
playlists={playlists}
selectedItem={selectedItem}
onPlaylistCreate={handleCreatePlaylist}
onPlaylistSelect={setSelectedItem}
/>
</Box>
<Box flex={1}>
<SongList
songs={displayedSongs}
onAddToPlaylist={handleAddSongToPlaylist}
playlists={playlists}
/>
</Box>
</Flex>
{songs.length > 0 && (
<Button onClick={handleExport} mt={4} colorScheme="blue">
Export XML
</Button>
)}
</Box>
);
}

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,89 @@
import {
Button,
Input,
Flex,
Text,
useDisclosure,
} from "@chakra-ui/react";
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
} from "@chakra-ui/modal";
import { useState } from "react";
import { Playlist } from "../types/interfaces";
interface PlaylistManagerProps {
playlists: Playlist[];
selectedItem: string | null;
onPlaylistCreate: (name: string) => void;
onPlaylistSelect: (name: string) => void;
}
export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
playlists,
selectedItem,
onPlaylistCreate,
onPlaylistSelect,
}) => {
const disclosure = useDisclosure();
const [newPlaylistName, setNewPlaylistName] = useState("");
const handleCreatePlaylist = () => {
if (newPlaylistName.trim()) {
onPlaylistCreate(newPlaylistName);
setNewPlaylistName("");
disclosure.onClose();
}
};
return (
<>
<Flex direction="column" gap={2}>
<Button
onClick={() => onPlaylistSelect("All Songs")}
colorScheme={selectedItem === "All Songs" ? "blue" : "gray"}
>
All Songs
</Button>
{playlists.map((playlist) => (
<Button
key={playlist.name}
onClick={() => onPlaylistSelect(playlist.name)}
colorScheme={selectedItem === playlist.name ? "blue" : "gray"}
>
{playlist.name}
</Button>
))}
<Button onClick={disclosure.onOpen} colorScheme="green">
Create New Playlist
</Button>
</Flex>
<Modal isOpen={disclosure.open} onClose={disclosure.onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Create New Playlist</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Input
value={newPlaylistName}
onChange={(e) => setNewPlaylistName(e.target.value)}
placeholder="Playlist name"
/>
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={handleCreatePlaylist}>
Create
</Button>
<Button onClick={disclosure.onClose}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};

View File

@ -0,0 +1,32 @@
import { Box, Flex, Text } from "@chakra-ui/react";
import { Song } from "../types/interfaces";
interface SongListProps {
songs: Song[];
onAddToPlaylist: (songId: string, playlistName: string) => void;
playlists: { name: string }[];
}
export const SongList: React.FC<SongListProps> = ({ songs, onAddToPlaylist, playlists }) => {
return (
<Flex direction="column" gap={4}>
{songs.map((song) => (
<Box key={song.id} borderWidth="1px" borderRadius="lg" p={4} shadow="md">
<Text fontWeight="bold">{song.title}</Text>
<Text color="gray.500">{song.artist}</Text>
<select
onChange={(e) => onAddToPlaylist(song.id, e.target.value)}
value=""
>
<option value="" disabled>Add to playlist</option>
{playlists.map((playlist) => (
<option key={playlist.name} value={playlist.name}>
{playlist.name}
</option>
))}
</select>
</Box>
))}
</Flex>
);
};

View File

@ -0,0 +1,64 @@
import { useState, useEffect } from "react";
import { Song, Playlist } from "../types/interfaces";
import { parseXmlFile } from "../services/xmlService";
import { api } from "../services/api";
export const useXmlParser = () => {
const [songs, setSongs] = useState<Song[]>([]);
const [playlists, setPlaylists] = useState<Playlist[]>([]);
const [loading, setLoading] = useState(true);
// Load data from the backend on mount
useEffect(() => {
const loadData = async () => {
try {
const [loadedSongs, loadedPlaylists] = await Promise.all([
api.getSongs(),
api.getPlaylists()
]);
setSongs(loadedSongs);
setPlaylists(loadedPlaylists);
} catch (err) {
console.error("Error loading data from backend:", err);
} finally {
setLoading(false);
}
};
loadData();
}, []);
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
const xmlText = e.target?.result as string;
try {
const { songs: parsedSongs, playlists: parsedPlaylists } = await parseXmlFile(xmlText);
// Save to backend
const [savedSongs, savedPlaylists] = await Promise.all([
api.saveSongs(parsedSongs),
api.savePlaylists(parsedPlaylists)
]);
setSongs(savedSongs);
setPlaylists(savedPlaylists);
} catch (err) {
console.error("Error processing XML:", err);
}
};
reader.readAsText(file);
};
return {
songs,
playlists,
setSongs,
setPlaylists,
handleFileUpload,
loading
};
};

View File

@ -0,0 +1,57 @@
import { Song, Playlist } from '../types/interfaces';
const API_URL = 'http://localhost:3000/api';
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'An error occurred' }));
throw new Error(error.message || 'An error occurred');
}
return response.json();
}
export const api = {
async getSongs(): Promise<Song[]> {
console.log('Fetching songs from API...');
const response = await fetch(`${API_URL}/songs`);
const data = await handleResponse<Song[]>(response);
console.log(`Received ${data.length} songs from API`);
return data;
},
async saveSongs(songs: Song[]): Promise<Song[]> {
console.log(`Saving ${songs.length} songs to API...`);
const response = await fetch(`${API_URL}/songs/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(songs),
});
const data = await handleResponse<Song[]>(response);
console.log(`Successfully saved ${data.length} songs`);
return data;
},
async getPlaylists(): Promise<Playlist[]> {
console.log('Fetching playlists from API...');
const response = await fetch(`${API_URL}/playlists`);
const data = await handleResponse<Playlist[]>(response);
console.log(`Received ${data.length} playlists from API`);
return data;
},
async savePlaylists(playlists: Playlist[]): Promise<Playlist[]> {
console.log(`Saving ${playlists.length} playlists to API...`);
const response = await fetch(`${API_URL}/playlists/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(playlists),
});
const data = await handleResponse<Playlist[]>(response);
console.log(`Successfully saved ${data.length} playlists`);
return data;
},
};

View File

@ -0,0 +1,120 @@
import { parseStringPromise } from "xml2js";
import { create } from "xmlbuilder";
import { Song, Playlist } from "../types/interfaces";
export const parseXmlFile = async (xmlText: string): Promise<{ songs: Song[], playlists: Playlist[] }> => {
try {
const result = await parseStringPromise(xmlText);
const tracks: Song[] = result.DJ_PLAYLISTS.COLLECTION[0].TRACK.map((track: any) => ({
id: track.$.TrackID,
title: track.$.Name,
artist: track.$.Artist,
composer: track.$.Composer,
album: track.$.Album,
grouping: track.$.Grouping,
genre: track.$.Genre,
kind: track.$.Kind,
size: track.$.Size,
totalTime: track.$.TotalTime,
discNumber: track.$.DiscNumber,
trackNumber: track.$.TrackNumber,
year: track.$.Year,
averageBpm: track.$.AverageBpm,
dateAdded: track.$.DateAdded,
bitRate: track.$.BitRate,
sampleRate: track.$.SampleRate,
comments: track.$.Comments,
playCount: track.$.PlayCount,
rating: track.$.Rating,
location: track.$.Location,
remixer: track.$.Remixer,
tonality: track.$.Tonality,
label: track.$.Label,
mix: track.$.Mix,
tempo: track.TEMPO ? {
inizio: track.TEMPO[0].$.Inizio,
bpm: track.TEMPO[0].$.Bpm,
metro: track.TEMPO[0].$.Metro,
battito: track.TEMPO[0].$.Battito,
} : undefined,
}));
const playlists: Playlist[] = result.DJ_PLAYLISTS.PLAYLISTS[0].NODE[0].NODE.map((playlist: any) => ({
name: playlist.$.Name,
tracks: playlist.TRACK ? playlist.TRACK.map((t: any) => t.$.Key) : [],
}));
return { songs: tracks, playlists };
} catch (err) {
console.error("Error parsing XML", err);
throw err;
}
};
export const exportToXml = (songs: Song[], playlists: Playlist[]): string => {
const doc = create("DJ_PLAYLISTS", { Version: "1.0.0" });
const collection = doc.ele("COLLECTION", {
Entries: songs.length.toString(),
});
songs.forEach((song) => {
const trackElement = collection.ele("TRACK", {
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 || "",
});
if (song.tempo) {
trackElement.ele("TEMPO", {
Inizio: song.tempo.inizio || "",
Bpm: song.tempo.bpm || "",
Metro: song.tempo.metro || "",
Battito: song.tempo.battito || "",
});
}
});
const playlistsNode = doc.ele("PLAYLISTS");
const playlistsFolder = playlistsNode.ele("NODE", {
Name: "ROOT",
Type: "0",
Count: playlists.length.toString(),
});
playlists.forEach((playlist) => {
const playlistNode = playlistsFolder.ele("NODE", {
Name: playlist.name,
KeyType: "0",
Type: "1",
Entries: playlist.tracks.length.toString(),
});
playlist.tracks.forEach((trackId) => {
playlistNode.ele("TRACK", { Key: trackId });
});
});
return doc.end({ pretty: true });
};

View File

@ -0,0 +1,38 @@
export interface Song {
id: string;
title: string;
artist: string;
composer?: string;
album?: string;
grouping?: string;
genre?: string;
kind?: string;
size?: string;
totalTime?: string;
discNumber?: string;
trackNumber?: string;
year?: string;
averageBpm?: string;
dateAdded?: string;
bitRate?: string;
sampleRate?: string;
comments?: string;
playCount?: string;
rating?: string;
location?: string;
remixer?: string;
tonality?: string;
label?: string;
mix?: string;
tempo?: {
inizio?: string;
bpm?: string;
metro?: string;
battito?: string;
};
}
export interface Playlist {
name: string;
tracks: string[];
}

View File

@ -1,332 +0,0 @@
import { useState } from "react";
import { parseStringPromise } from "xml2js";
import {
Box,
Button,
Flex,
Heading,
Input,
Stack,
Text,
VStack,
useDisclosure,
} from "@chakra-ui/react";
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
} from "@chakra-ui/modal";
import { create } from "xmlbuilder";
interface Song {
id: string;
title: string;
artist: string;
composer?: string;
album?: string;
grouping?: string;
genre?: string;
kind?: string;
size?: string;
totalTime?: string;
discNumber?: string;
trackNumber?: string;
year?: string;
averageBpm?: string;
dateAdded?: string;
bitRate?: string;
sampleRate?: string;
comments?: string;
playCount?: string;
rating?: string;
location?: string;
remixer?: string;
tonality?: string;
label?: string;
mix?: string;
tempo?: {
inizio?: string;
bpm?: string;
metro?: string;
battito?: string;
};
}
interface Playlist {
name: string;
tracks: string[];
}
export default function RekordboxReader() {
const [songs, setSongs] = useState<Song[]>([]);
const [playlists, setPlaylists] = useState<Playlist[]>([]);
const [selectedItem, setSelectedItem] = useState<string | null>("All Songs");
const { open, onOpen, onClose } = useDisclosure();
const [newPlaylistName, setNewPlaylistName] = useState("");
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
const xmlText = e.target?.result as string;
try {
const result = await parseStringPromise(xmlText);
console.log("Parsed XML:", result);
const tracks: Song[] = result.DJ_PLAYLISTS.COLLECTION[0].TRACK.map((track: any) => ({
id: track.$.TrackID,
title: track.$.Name,
artist: track.$.Artist,
composer: track.$.Composer,
album: track.$.Album,
grouping: track.$.Grouping,
genre: track.$.Genre,
kind: track.$.Kind,
size: track.$.Size,
totalTime: track.$.TotalTime,
discNumber: track.$.DiscNumber,
trackNumber: track.$.TrackNumber,
year: track.$.Year,
averageBpm: track.$.AverageBpm,
dateAdded: track.$.DateAdded,
bitRate: track.$.BitRate,
sampleRate: track.$.SampleRate,
comments: track.$.Comments,
playCount: track.$.PlayCount,
rating: track.$.Rating,
location: track.$.Location,
remixer: track.$.Remixer,
tonality: track.$.Tonality,
label: track.$.Label,
mix: track.$.Mix,
tempo: track.TEMPO ? {
inizio: track.TEMPO[0].$.Inizio,
bpm: track.TEMPO[0].$.Bpm,
metro: track.TEMPO[0].$.Metro,
battito: track.TEMPO[0].$.Battito,
} : undefined,
}));
const lists: Playlist[] = result.DJ_PLAYLISTS.PLAYLISTS[0].NODE[0].NODE.map((playlist: any) => ({
name: playlist.$.Name,
tracks: playlist.TRACK ? playlist.TRACK.map((t: any) => t.$.Key) : [],
}));
console.log("Tracks:", tracks);
console.log("Playlists:", lists);
setSongs(tracks);
setPlaylists(lists);
} catch (err) {
console.error("Error parsing XML", err);
}
};
reader.readAsText(file);
};
const handleMenuItemClick = (item: string) => {
setSelectedItem(item);
};
const handleCreatePlaylist = () => {
if (newPlaylistName.trim()) {
setPlaylists([...playlists, { name: newPlaylistName, tracks: [] }]);
setNewPlaylistName("");
onClose();
}
};
const handleAddSongToPlaylist = (songId: string, playlistName: string) => {
const updatedPlaylists = playlists.map((playlist) =>
playlist.name === playlistName
? { ...playlist, tracks: [...playlist.tracks, songId] }
: playlist
);
setPlaylists(updatedPlaylists);
};
const exportToXML = () => {
const doc = create("DJ_PLAYLISTS", { Version: "1.0.0" });
const collection = doc.ele("COLLECTION", {
Entries: songs.length.toString(),
});
// Add tracks to the collection with all metadata
songs.forEach((song) => {
const trackElement = collection.ele("TRACK", {
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 || "",
});
// Add the TEMPO nested element if available
if (song.tempo) {
trackElement.ele("TEMPO", {
Inizio: song.tempo.inizio || "",
Bpm: song.tempo.bpm || "",
Metro: song.tempo.metro || "",
Battito: song.tempo.battito || "",
});
}
});
// Create the playlists structure
const playlistsNode = doc.ele("PLAYLISTS");
const playlistsFolder = playlistsNode.ele("NODE", {
Name: "ROOT",
Type: "0",
Count: playlists.length.toString(),
});
// Add each playlist and its tracks
playlists.forEach((playlist) => {
const playlistNode = playlistsFolder.ele("NODE", {
Name: playlist.name,
KeyType: "0",
Type: "1",
Entries: playlist.tracks.length.toString(),
});
playlist.tracks.forEach((trackId) => {
playlistNode.ele("TRACK", { Key: trackId });
});
});
// Generate the XML content as a string
const xmlContent = doc.end({ pretty: true });
// Create a Blob from the XML content
const blob = new Blob([xmlContent], { type: "application/xml" });
const url = URL.createObjectURL(blob);
// Create a download link and trigger the download
const a = document.createElement("a");
a.href = url;
a.download = "rekordbox_playlists.xml";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const renderContent = () => {
if (selectedItem === "All Songs") {
return (
<Stack spacing={4} align="stretch">
{songs.map((song) => (
<Box key={song.id} borderWidth="1px" borderRadius="lg" p={4} shadow="md">
<Text fontWeight="bold">{song.title}</Text>
<Text color="gray.500">{song.artist}</Text>
<select
onChange={(e) => handleAddSongToPlaylist(song.id, e.target.value)}
value=""
>
<option value="" disabled>Add to playlist</option>
{playlists.map((playlist) => (
<option key={playlist.name} value={playlist.name}>
{playlist.name}
</option>
))}
</select>
</Box>
))}
</Stack>
);
} else {
const selectedPlaylist = playlists.find((playlist) => playlist.name === selectedItem);
return (
<Stack spacing={4} align="stretch">
{selectedPlaylist?.tracks.map((trackId) => {
const song = songs.find((s) => s.id === trackId);
return song ? (
<Box key={trackId} borderWidth="1px" borderRadius="lg" p={4} shadow="md">
<Text fontWeight="bold">{song.title}</Text>
<Text color="gray.500">{song.artist}</Text>
</Box>
) : null;
})}
</Stack>
);
}
};
return (
<Flex h="100vh">
<VStack w="200px" borderRight="1px" p={4}>
<Button onClick={() => handleMenuItemClick("All Songs")} w="100%">
All Songs
</Button>
{playlists.map((playlist) => (
<Button
key={playlist.name}
onClick={() => handleMenuItemClick(playlist.name)}
w="100%"
>
{playlist.name}
</Button>
))}
<Button onClick={onOpen} w="100%">
Create New Playlist
</Button>
</VStack>
<Box p={4} flex="1">
<Input type="file" accept=".xml" onChange={handleFileUpload} mb={4} />
<Button onClick={exportToXML} mb={4}>
Export to XML
</Button>
<Heading size="lg" mb={4}>
{selectedItem}
</Heading>
{renderContent()}
</Box>
<Modal isOpen={open} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Create New Playlist</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Input
placeholder="Playlist Name"
value={newPlaylistName}
onChange={(e) => setNewPlaylistName(e.target.value)}
/>
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={handleCreatePlaylist}>
Create
</Button>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Flex>
);
}