rekordcrate/
xml.rs

1// Copyright (c) 2025 Jan Holthuis <jan.holthuis@rub.de>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy
4// of the MPL was not distributed with this file, You can obtain one at
5// http://mozilla.org/MPL/2.0/.
6//
7// SPDX-License-Identifier: MPL-2.0
8
9//! Parser for the Rekordbox XML file format for playlists sharing.
10//!
11//! The XML format includes all playlists information.
12//!
13//! # References
14//!
15//! - <https://rekordbox.com/en/support/developer/>
16//! - <https://cdn.rekordbox.com/files/20200410160904/xml_format_list.pdf>
17//! - <https://pyrekordbox.readthedocs.io/en/stable/formats/xml.html>
18type NaiveDate = String; //Replace with "use chrono::naive::NaiveDate;"
19use serde::{de::Error, ser::Serializer, Deserialize, Serialize};
20use std::borrow::Cow;
21
22/// The XML root element of a rekordbox XML file.
23#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
24#[serde(rename = "DJ_PLAYLISTS")]
25pub struct Document {
26    /// Version of the XML format for share the playlists.
27    ///
28    /// The latest version is 1,0,0.
29    #[serde(rename = "@Version")]
30    version: String,
31    #[serde(rename = "PRODUCT")]
32    product: Product,
33    #[serde(rename = "COLLECTION")]
34    collection: Collection,
35    #[serde(rename = "PLAYLISTS")]
36    playlists: Playlists,
37}
38
39#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
40struct Product {
41    /// Name of product
42    ///
43    /// This name will be displayed in each application software.
44    #[serde(rename = "@Name")]
45    name: String,
46    /// Version of application
47    #[serde(rename = "@Version")]
48    version: String,
49    /// Name of company
50    #[serde(rename = "@Company")]
51    company: String,
52}
53
54/// The information of the tracks who are not included in any playlist are unnecessary.
55#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
56struct Collection {
57    /// Number of TRACK in COLLECTION
58    #[serde(rename = "@Entries")]
59    entries: i32,
60    #[serde(rename = "TRACK")]
61    track: Vec<Track>,
62}
63
64/// "Location" is essential for each track ;
65#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
66struct Track {
67    /// Identification of track
68    #[serde(rename = "@TrackID")]
69    trackid: i32,
70    /// Name of track
71    #[serde(rename = "@Name")]
72    name: Option<String>,
73    /// Name of artist
74    #[serde(rename = "@Artist")]
75    artist: Option<String>,
76    /// Name of composer (or producer)
77    #[serde(rename = "@Composer")]
78    composer: Option<String>,
79    /// Name of Album
80    #[serde(rename = "@Album")]
81    album: Option<String>,
82    /// Name of goupe
83    #[serde(rename = "@Grouping")]
84    grouping: Option<String>,
85    /// Name of genre
86    #[serde(rename = "@Genre")]
87    genre: Option<String>,
88    /// Type of audio file
89    #[serde(rename = "@Kind")]
90    kind: Option<String>,
91    /// Size of audio file
92    /// Unit : Octet
93    #[serde(rename = "@Size")]
94    size: Option<i64>,
95    /// Duration of track
96    /// Unit : Second (without decimal numbers)
97    #[serde(rename = "@TotalTime")]
98    totaltime: Option<f64>,
99    /// Order number of the disc of the album
100    #[serde(rename = "@DiscNumber")]
101    discnumber: Option<i32>,
102    /// Order number of the track in the album
103    #[serde(rename = "@TrackNumber")]
104    tracknumber: Option<i32>,
105    /// Year of release
106    #[serde(rename = "@Year")]
107    year: Option<i32>,
108    /// Value of average BPM
109    /// Unit : Second (with decimal numbers)
110    #[serde(rename = "@AverageBpm")]
111    averagebpm: Option<f64>,
112    /// Date of last modification
113    /// Format : yyyy- mm- dd ; ex. : 2010- 08- 21
114    #[serde(rename = "@DateModified")]
115    #[serde(skip_serializing_if = "Option::is_none")]
116    datemodified: Option<NaiveDate>,
117    /// Date of addition
118    /// Format : yyyy- mm- dd ; ex. : 2010- 08- 21
119    #[serde(rename = "@DateAdded")]
120    #[serde(skip_serializing_if = "Option::is_none")]
121    dateadded: Option<NaiveDate>,
122    /// Encoding bit rate
123    /// Unit : Kbps
124    #[serde(rename = "@BitRate")]
125    bitrate: Option<i32>,
126    /// Frequency of sampling
127    /// Unit : Hertz
128    #[serde(rename = "@SampleRate")]
129    samplerate: Option<f64>,
130    /// Comments
131    #[serde(rename = "@Comments")]
132    comments: Option<String>,
133    /// Play count of the track
134    #[serde(rename = "@PlayCount")]
135    playcount: Option<i32>,
136    /// Date of last playing
137    /// Format : yyyy- mm- dd ; ex. : 2010- 08- 21
138    #[serde(rename = "@LastPlayed")]
139    #[serde(skip_serializing_if = "Option::is_none")]
140    lastplayed: Option<NaiveDate>,
141    /// Rating of the track
142    /// 0 star = "@0", 1 star = "51", 2 stars = "102", 3 stars = "153", 4 stars = "204", 5 stars = "255"
143    #[serde(rename = "@Rating")]
144    rating: Option<i32>,
145    /// Location of the file
146    /// includes the file name (URI formatted)
147    #[serde(rename = "@Location")]
148    location: String,
149    /// Name of remixer
150    #[serde(rename = "@Remixer")]
151    remixer: Option<String>,
152    /// Tonality (Kind of musical key)
153    #[serde(rename = "@Tonality")]
154    tonality: Option<String>,
155    /// Name of record label
156    #[serde(rename = "@Label")]
157    label: Option<String>,
158    /// Name of mix
159    #[serde(rename = "@Mix")]
160    mix: Option<String>,
161    /// Colour for track grouping
162    /// RGB format (3 bytes) ; rekordbox : Rose(0xFF007F), Red(0xFF0000), Orange(0xFFA500), Lemon(0xFFFF00), Green(0x00FF00), Turquoise(0x25FDE9),  Blue(0x0000FF), Violet(0x660099)
163    #[serde(rename = "@Colour")]
164    #[serde(skip_serializing_if = "Option::is_none")]
165    colour: Option<String>,
166    #[serde(rename = "TEMPO")]
167    #[serde(skip_serializing_if = "Vec::is_empty")]
168    #[serde(default)]
169    tempos: Vec<Tempo>,
170    #[serde(rename = "POSITION_MARK")]
171    #[serde(skip_serializing_if = "Vec::is_empty")]
172    #[serde(default)]
173    position_marks: Vec<PositionMark>,
174}
175
176/// 0 star = "@0", 1 star = "51", 2 stars = "102", 3 stars = "153", 4 stars = "204", 5 stars = "255"
177#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
178enum StarRating {
179    Zero,
180    One,
181    Two,
182    Three,
183    Four,
184    Five,
185    Unknown(i32),
186}
187
188/// For BeatGrid; More than two "TEMPO" can exist for each track
189#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
190struct Tempo {
191    /// Start position of BeatGrid
192    /// Unit : Second (with decimal numbers)
193    #[serde(rename = "@Inizio")]
194    inizio: f64,
195    /// Value of BPM
196    /// Unit : Second (with decimal numbers)
197    #[serde(rename = "@Bpm")]
198    bpm: f64,
199    /// Kind of musical meter (formatted)
200    /// ex. 3/ 4, 4/ 4, 7/ 8…
201    #[serde(rename = "@Metro")]
202    metro: String,
203    /// Beat number in the bar
204    /// If the value of "Metro" is 4/ 4, the value should be 1, 2, 3 or 4.
205    #[serde(rename = "@Battito")]
206    battito: i32,
207}
208
209/// More than two "POSITION MARK" can exist for each track
210#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
211struct PositionMark {
212    /// Name of position mark
213    #[serde(rename = "@Name")]
214    name: String,
215    /// Type of position mark
216    /// Cue = "@0", Fade- In = "1", Fade- Out = "2", Load = "3",  Loop = " 4"
217    #[serde(rename = "@Type")]
218    mark_type: i32,
219    /// Start position of position mark
220    /// Unit : Second (with decimal numbers)
221    #[serde(rename = "@Start")]
222    start: f64,
223    /// End position of position mark
224    /// Unit : Second (with decimal numbers)
225    #[serde(rename = "@End")]
226    #[serde(skip_serializing_if = "Option::is_none")]
227    end: Option<f64>,
228    /// Number for identification of the position mark
229    /// rekordbox : Hot Cue A,  B,  C : "0", "1", "2"; Memory Cue : "- 1"
230    #[serde(rename = "@Num")]
231    num: i32,
232}
233
234#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
235struct Playlists {
236    #[serde(rename = "NODE")]
237    node: PlaylistFolderNode,
238}
239
240#[derive(Debug, PartialEq, Clone, Serialize)]
241#[serde(tag = "@Type")]
242enum PlaylistGenericNode {
243    #[serde(rename = "0")]
244    Folder(PlaylistFolderNode),
245    #[serde(rename = "1")]
246    Playlist(PlaylistPlaylistNode),
247}
248
249impl<'de> Deserialize<'de> for PlaylistGenericNode {
250    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
251    where
252        D: serde::Deserializer<'de>,
253    {
254        struct PlaylistGenericNodeVisitor;
255
256        impl<'de> serde::de::Visitor<'de> for PlaylistGenericNodeVisitor {
257            type Value = PlaylistGenericNode;
258
259            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260                formatter.write_str("struct PlaylistGenericNode")
261            }
262
263            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
264            where
265                A: serde::de::MapAccess<'de>,
266            {
267                let mut node_type = None;
268                let mut name = None;
269                let mut count = None;
270                let mut key_type = None;
271                let mut entries = None;
272
273                while let Some(key) = map.next_key::<Cow<'_, str>>()? {
274                    match key.as_ref() {
275                        "@Name" => name = map.next_value::<Cow<'_, str>>()?.into(),
276                        "@Type" => node_type = map.next_value::<Cow<'_, str>>()?.into(),
277                        "@Count" => count = map.next_value::<usize>()?.into(),
278                        "@KeyType" => key_type = map.next_value::<Cow<'_, str>>()?.into(),
279                        "@Entries" => entries = map.next_value::<usize>()?.into(),
280                        unknown => {
281                            return Err(A::Error::unknown_field(
282                                unknown,
283                                &["@Name", "@Type", "@Count", "@KeyType", "@Entries"],
284                            ));
285                        }
286                    }
287
288                    match node_type.as_deref() {
289                        Some("0") => {
290                            if let (Some(n), Some(_c)) = (&name, count) {
291                                let nodes = {
292                                    // Create anonymous type
293                                    #[derive(serde::Deserialize)]
294                                    struct Nodes {
295                                        #[serde(rename = "NODE")]
296                                        content: Vec<PlaylistGenericNode>,
297                                    }
298                                    let de = serde::de::value::MapAccessDeserializer::new(map);
299                                    Nodes::deserialize(de)?.content
300                                };
301                                // FIXME: Should we check if nodes.len() == count here?
302                                return Ok(PlaylistGenericNode::Folder(PlaylistFolderNode {
303                                    name: n.to_string(),
304                                    nodes,
305                                }));
306                            }
307                        }
308                        Some("1") => {
309                            if let (Some(n), Some(_c), Some(t)) = (&name, entries, &key_type) {
310                                let tracks = {
311                                    // Create anonymous type
312                                    #[derive(serde::Deserialize)]
313                                    struct Tracks {
314                                        #[serde(rename = "TRACK")]
315                                        content: Vec<PlaylistTrack>,
316                                    }
317                                    let de = serde::de::value::MapAccessDeserializer::new(map);
318                                    Tracks::deserialize(de)?.content
319                                };
320                                // FIXME: Should we check if nodes.len() == count here?
321                                return Ok(PlaylistGenericNode::Playlist(PlaylistPlaylistNode {
322                                    name: n.to_string(),
323                                    keytype: t.to_string(),
324                                    tracks,
325                                }));
326                            }
327                        }
328                        Some(unknown) => {
329                            return Err(A::Error::unknown_variant(unknown, &["0", "1"]))
330                        }
331                        None => (),
332                    }
333                }
334
335                match node_type.as_deref() {
336                    Some("0") => {
337                        if name.is_none() {
338                            Err(A::Error::missing_field("@Name"))
339                        } else {
340                            Err(A::Error::missing_field("@Count"))
341                        }
342                    }
343                    Some("1") => {
344                        if name.is_none() {
345                            Err(A::Error::missing_field("@Name"))
346                        } else if entries.is_none() {
347                            Err(A::Error::missing_field("@Entries"))
348                        } else {
349                            Err(A::Error::missing_field("@KeyType"))
350                        }
351                    }
352                    _ => Err(A::Error::missing_field("@Type")),
353                }
354            }
355        }
356
357        deserializer.deserialize_map(PlaylistGenericNodeVisitor)
358    }
359}
360
361#[derive(Debug, PartialEq, Clone, Deserialize)]
362struct PlaylistFolderNode {
363    /// Name of NODE
364    #[serde(rename = "@Name")]
365    name: String,
366    // The "Count" attribute that contains the "Number of NODE in NODE" is omitted here, because we
367    // can just take the number of elements in the `tracks` vector instead.
368    /// Nodes
369    #[serde(rename = "NODE")]
370    nodes: Vec<PlaylistGenericNode>,
371}
372
373impl Serialize for PlaylistFolderNode {
374    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
375    where
376        S: Serializer,
377    {
378        #[derive(Serialize)]
379        struct Value<'a> {
380            /// Name of NODE
381            #[serde(rename = "@Name")]
382            name: &'a String,
383            /// Count
384            #[serde(rename = "@Count")]
385            count: usize,
386            /// Nodes
387            #[serde(rename = "NODE")]
388            nodes: &'a Vec<PlaylistGenericNode>,
389        }
390
391        let value = Value {
392            name: &self.name,
393            count: self.nodes.len(),
394            nodes: &self.nodes,
395        };
396
397        value.serialize(serializer)
398    }
399}
400
401#[derive(Debug, PartialEq, Clone, Deserialize)]
402struct PlaylistPlaylistNode {
403    /// Name of NODE
404    #[serde(rename = "@Name")]
405    name: String,
406    // The "Entries" attribute that contains the "Number of TRACK in PLAYLIST" is omitted here,
407    // because we can just take the number of elements in the `tracks` vector instead.
408    /// Kind of identification
409    /// "0" (Track ID) or "1"(Location)
410    #[serde(rename = "@KeyType")]
411    keytype: String,
412    #[serde(rename = "TRACK")]
413    tracks: Vec<PlaylistTrack>,
414}
415
416impl Serialize for PlaylistPlaylistNode {
417    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
418    where
419        S: Serializer,
420    {
421        #[derive(Serialize)]
422        struct Value<'a> {
423            /// Name of NODE
424            #[serde(rename = "@Name")]
425            name: &'a String,
426            /// Number of TRACK in PLAYLIST
427            #[serde(rename = "@Entries")]
428            entries: usize,
429            /// Kind of identification
430            /// "0" (Track ID) or "1"(Location)
431            #[serde(rename = "@KeyType")]
432            keytype: &'a String,
433            #[serde(rename = "TRACK")]
434            tracks: &'a Vec<PlaylistTrack>,
435        }
436
437        let value = Value {
438            name: &self.name,
439            entries: self.tracks.len(),
440            keytype: &self.keytype,
441            tracks: &self.tracks,
442        };
443
444        value.serialize(serializer)
445    }
446}
447
448#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
449struct PlaylistTrack {
450    /// Identification of track
451    /// "Track ID" or "Location" in "COLLECTION"
452    #[serde(rename = "@Key")]
453    key: i32,
454}