From 8306045a39c92fdef87a0a63d97f9aa557d20e8f Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 27 Jul 2024 21:15:45 +0200 Subject: [PATCH] feat(xml): Add support for Rekordbox XML format --- .codespellignore | 1 + .pre-commit-config.yaml | 3 +- Cargo.lock | 279 ++++++++++++++++++++++-- Cargo.toml | 4 +- build.rs | 9 + data/xml/database.xml | 77 +++++++ src/lib.rs | 1 + src/main.rs | 17 ++ src/xml.rs | 454 ++++++++++++++++++++++++++++++++++++++++ tests/test_loader.rs | 1 + tests/tests_xml.rs.in | 10 + 11 files changed, 832 insertions(+), 24 deletions(-) create mode 100644 data/xml/database.xml create mode 100644 src/xml.rs create mode 100644 tests/tests_xml.rs.in diff --git a/.codespellignore b/.codespellignore index 9ac17d57..d84f1c41 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1 +1,2 @@ crate +ser diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ecfb4ed5..6c6a9a65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: --ignore-words=.codespellignore ] # We cannot fix spelling errors in past commit message without force-push. - exclude: ^CHANGELOG.md$ + exclude: ^(CHANGELOG\.md|data/.*)$ - repo: https://github.com/doublify/pre-commit-rust rev: v1.0 hooks: @@ -56,6 +56,7 @@ repos: rev: v0.0.4 hooks: - id: sourceheaders + exclude: ^data/.*$ - repo: https://github.com/jorisroovers/gitlint rev: v0.19.1 hooks: diff --git a/Cargo.lock b/Cargo.lock index a1388b3d..5d9a7384 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -52,11 +67,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys", ] @@ -66,6 +82,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "binrw" version = "0.14.1" @@ -90,17 +112,52 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + +[[package]] +name = "cc" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] [[package]] name = "clap" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -108,9 +165,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -127,7 +184,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -142,6 +199,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crc16" version = "0.4.0" @@ -172,12 +235,57 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + [[package]] name = "memchr" version = "2.7.4" @@ -205,6 +313,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "owo-colors" version = "3.5.0" @@ -233,7 +356,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -254,18 +377,28 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -304,6 +437,7 @@ name = "rekordcrate" version = "0.2.1" dependencies = [ "binrw", + "chrono", "clap", "crc16", "glob", @@ -311,9 +445,43 @@ dependencies = [ "parse-display", "pretty-hex", "pretty_assertions", + "quick-xml", + "serde", "thiserror", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "static_assertions" version = "1.1.0" @@ -335,7 +503,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -346,7 +514,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -362,9 +530,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -388,14 +556,14 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "utf8parse" @@ -403,6 +571,73 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 941bd1f5..519d2c1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,9 @@ crc16 = "0.4" clap = { version = "4.5", features = ["derive"], optional = true } parse-display = "0.10" thiserror = "2.0" - +quick-xml = { version = "0.37.2", features = ["serialize", "serde-types"] } +serde = { version = "1.0", features = ["derive"] } +chrono = "0.4" [build-dependencies] glob = "0.3" diff --git a/build.rs b/build.rs index 6e02d04a..74b26290 100644 --- a/build.rs +++ b/build.rs @@ -77,4 +77,13 @@ use rekordcrate::setting::Setting; "#, "./tests/tests_setting.rs.in" ); + + write_test!( + out_dir.join("tests_xml.rs"), + "data/xml/*.xml", + r#"// THIS FILE IS AUTOGENERATED - DO NOT EDIT! +use rekordcrate::xml::Document; +"#, + "./tests/tests_xml.rs.in" + ); } diff --git a/data/xml/database.xml b/data/xml/database.xml new file mode 100644 index 00000000..dd29426a --- /dev/null +++ b/data/xml/database.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib.rs b/src/lib.rs index 444fc4b2..d4530fdf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ pub mod anlz; pub mod pdb; pub mod setting; pub mod util; +pub mod xml; pub(crate) mod xor; pub use crate::util::RekordcrateError as Error; diff --git a/src/main.rs b/src/main.rs index 50406198..b4f66cbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use clap::{Parser, Subcommand}; use rekordcrate::anlz::ANLZ; use rekordcrate::pdb::{Header, PageType, Row}; use rekordcrate::setting::Setting; +use rekordcrate::xml::Document; use std::path::PathBuf; #[derive(Parser)] @@ -47,6 +48,12 @@ enum Commands { #[arg(value_name = "SETTING_FILE")] path: PathBuf, }, + /// Parse and dump a Pioneer XML (`*.xml`) file. + DumpXML { + /// File to parse. + #[arg(value_name = "XML_FILE")] + path: PathBuf, + }, } fn list_playlists(path: &PathBuf) -> rekordcrate::Result<()> { @@ -159,6 +166,15 @@ fn dump_setting(path: &PathBuf) -> rekordcrate::Result<()> { Ok(()) } +fn dump_xml(path: &PathBuf) -> rekordcrate::Result<()> { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + let document: Document = quick_xml::de::from_reader(reader).expect("failed to deserialize XML"); + println!("{:#?}", document); + + Ok(()) +} + fn main() -> rekordcrate::Result<()> { let cli = Cli::parse(); @@ -167,5 +183,6 @@ fn main() -> rekordcrate::Result<()> { Commands::DumpPDB { path } => dump_pdb(path), Commands::DumpANLZ { path } => dump_anlz(path), Commands::DumpSetting { path } => dump_setting(path), + Commands::DumpXML { path } => dump_xml(path), } } diff --git a/src/xml.rs b/src/xml.rs new file mode 100644 index 00000000..1ef24227 --- /dev/null +++ b/src/xml.rs @@ -0,0 +1,454 @@ +// Copyright (c) 2025 Jan Holthuis +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy +// of the MPL was not distributed with this file, You can obtain one at +// http://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 + +//! Parser for the Rekordbox XML file format for playlists sharing. +//! +//! The XML format includes all playlists information. +//! +//! # References +//! +//! - +//! - +//! - +type NaiveDate = String; //Replace with "use chrono::naive::NaiveDate;" +use serde::{de::Error, ser::Serializer, Deserialize, Serialize}; +use std::borrow::Cow; + +/// The XML root element of a rekordbox XML file. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename = "DJ_PLAYLISTS")] +pub struct Document { + /// Version of the XML format for share the playlists. + /// + /// The latest version is 1,0,0. + #[serde(rename = "@Version")] + version: String, + #[serde(rename = "PRODUCT")] + product: Product, + #[serde(rename = "COLLECTION")] + collection: Collection, + #[serde(rename = "PLAYLISTS")] + playlists: Playlists, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +struct Product { + /// Name of product + /// + /// This name will be displayed in each application software. + #[serde(rename = "@Name")] + name: String, + /// Version of application + #[serde(rename = "@Version")] + version: String, + /// Name of company + #[serde(rename = "@Company")] + company: String, +} + +/// The information of the tracks who are not included in any playlist are unnecessary. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +struct Collection { + /// Number of TRACK in COLLECTION + #[serde(rename = "@Entries")] + entries: i32, + #[serde(rename = "TRACK")] + track: Vec, +} + +/// "Location" is essential for each track ; +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +struct Track { + /// Identification of track + #[serde(rename = "@TrackID")] + trackid: i32, + /// Name of track + #[serde(rename = "@Name")] + name: Option, + /// Name of artist + #[serde(rename = "@Artist")] + artist: Option, + /// Name of composer (or producer) + #[serde(rename = "@Composer")] + composer: Option, + /// Name of Album + #[serde(rename = "@Album")] + album: Option, + /// Name of goupe + #[serde(rename = "@Grouping")] + grouping: Option, + /// Name of genre + #[serde(rename = "@Genre")] + genre: Option, + /// Type of audio file + #[serde(rename = "@Kind")] + kind: Option, + /// Size of audio file + /// Unit : Octet + #[serde(rename = "@Size")] + size: Option, + /// Duration of track + /// Unit : Second (without decimal numbers) + #[serde(rename = "@TotalTime")] + totaltime: Option, + /// Order number of the disc of the album + #[serde(rename = "@DiscNumber")] + discnumber: Option, + /// Order number of the track in the album + #[serde(rename = "@TrackNumber")] + tracknumber: Option, + /// Year of release + #[serde(rename = "@Year")] + year: Option, + /// Value of average BPM + /// Unit : Second (with decimal numbers) + #[serde(rename = "@AverageBpm")] + averagebpm: Option, + /// Date of last modification + /// Format : yyyy- mm- dd ; ex. : 2010- 08- 21 + #[serde(rename = "@DateModified")] + #[serde(skip_serializing_if = "Option::is_none")] + datemodified: Option, + /// Date of addition + /// Format : yyyy- mm- dd ; ex. : 2010- 08- 21 + #[serde(rename = "@DateAdded")] + #[serde(skip_serializing_if = "Option::is_none")] + dateadded: Option, + /// Encoding bit rate + /// Unit : Kbps + #[serde(rename = "@BitRate")] + bitrate: Option, + /// Frequency of sampling + /// Unit : Hertz + #[serde(rename = "@SampleRate")] + samplerate: Option, + /// Comments + #[serde(rename = "@Comments")] + comments: Option, + /// Play count of the track + #[serde(rename = "@PlayCount")] + playcount: Option, + /// Date of last playing + /// Format : yyyy- mm- dd ; ex. : 2010- 08- 21 + #[serde(rename = "@LastPlayed")] + #[serde(skip_serializing_if = "Option::is_none")] + lastplayed: Option, + /// Rating of the track + /// 0 star = "@0", 1 star = "51", 2 stars = "102", 3 stars = "153", 4 stars = "204", 5 stars = "255" + #[serde(rename = "@Rating")] + rating: Option, + /// Location of the file + /// includes the file name (URI formatted) + #[serde(rename = "@Location")] + location: String, + /// Name of remixer + #[serde(rename = "@Remixer")] + remixer: Option, + /// Tonality (Kind of musical key) + #[serde(rename = "@Tonality")] + tonality: Option, + /// Name of record label + #[serde(rename = "@Label")] + label: Option, + /// Name of mix + #[serde(rename = "@Mix")] + mix: Option, + /// Colour for track grouping + /// RGB format (3 bytes) ; rekordbox : Rose(0xFF007F), Red(0xFF0000), Orange(0xFFA500), Lemon(0xFFFF00), Green(0x00FF00), Turquoise(0x25FDE9), Blue(0x0000FF), Violet(0x660099) + #[serde(rename = "@Colour")] + #[serde(skip_serializing_if = "Option::is_none")] + colour: Option, + #[serde(rename = "TEMPO")] + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + tempos: Vec, + #[serde(rename = "POSITION_MARK")] + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + position_marks: Vec, +} + +/// 0 star = "@0", 1 star = "51", 2 stars = "102", 3 stars = "153", 4 stars = "204", 5 stars = "255" +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] +enum StarRating { + Zero, + One, + Two, + Three, + Four, + Five, + Unknown(i32), +} + +/// For BeatGrid; More than two "TEMPO" can exist for each track +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +struct Tempo { + /// Start position of BeatGrid + /// Unit : Second (with decimal numbers) + #[serde(rename = "@Inizio")] + inizio: f64, + /// Value of BPM + /// Unit : Second (with decimal numbers) + #[serde(rename = "@Bpm")] + bpm: f64, + /// Kind of musical meter (formatted) + /// ex. 3/ 4, 4/ 4, 7/ 8… + #[serde(rename = "@Metro")] + metro: String, + /// Beat number in the bar + /// If the value of "Metro" is 4/ 4, the value should be 1, 2, 3 or 4. + #[serde(rename = "@Battito")] + battito: i32, +} + +/// More than two "POSITION MARK" can exist for each track +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +struct PositionMark { + /// Name of position mark + #[serde(rename = "@Name")] + name: String, + /// Type of position mark + /// Cue = "@0", Fade- In = "1", Fade- Out = "2", Load = "3", Loop = " 4" + #[serde(rename = "@Type")] + mark_type: i32, + /// Start position of position mark + /// Unit : Second (with decimal numbers) + #[serde(rename = "@Start")] + start: f64, + /// End position of position mark + /// Unit : Second (with decimal numbers) + #[serde(rename = "@End")] + #[serde(skip_serializing_if = "Option::is_none")] + end: Option, + /// Number for identification of the position mark + /// rekordbox : Hot Cue A, B, C : "0", "1", "2"; Memory Cue : "- 1" + #[serde(rename = "@Num")] + num: i32, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +struct Playlists { + #[serde(rename = "NODE")] + node: PlaylistFolderNode, +} + +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(tag = "@Type")] +enum PlaylistGenericNode { + #[serde(rename = "0")] + Folder(PlaylistFolderNode), + #[serde(rename = "1")] + Playlist(PlaylistPlaylistNode), +} + +impl<'de> Deserialize<'de> for PlaylistGenericNode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct PlaylistGenericNodeVisitor; + + impl<'de> serde::de::Visitor<'de> for PlaylistGenericNodeVisitor { + type Value = PlaylistGenericNode; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct PlaylistGenericNode") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut node_type = None; + let mut name = None; + let mut count = None; + let mut key_type = None; + let mut entries = None; + + while let Some(key) = map.next_key::>()? { + match key.as_ref() { + "@Name" => name = map.next_value::>()?.into(), + "@Type" => node_type = map.next_value::>()?.into(), + "@Count" => count = map.next_value::()?.into(), + "@KeyType" => key_type = map.next_value::>()?.into(), + "@Entries" => entries = map.next_value::()?.into(), + unknown => { + return Err(A::Error::unknown_field( + unknown, + &["@Name", "@Type", "@Count", "@KeyType", "@Entries"], + )); + } + } + + match node_type.as_deref() { + Some("0") => { + if let (Some(n), Some(_c)) = (&name, count) { + let nodes = { + // Create anonymous type + #[derive(serde::Deserialize)] + struct Nodes { + #[serde(rename = "NODE")] + content: Vec, + } + let de = serde::de::value::MapAccessDeserializer::new(map); + Nodes::deserialize(de)?.content + }; + // FIXME: Should we check if nodes.len() == count here? + return Ok(PlaylistGenericNode::Folder(PlaylistFolderNode { + name: n.to_string(), + nodes, + })); + } + } + Some("1") => { + if let (Some(n), Some(_c), Some(t)) = (&name, entries, &key_type) { + let tracks = { + // Create anonymous type + #[derive(serde::Deserialize)] + struct Tracks { + #[serde(rename = "TRACK")] + content: Vec, + } + let de = serde::de::value::MapAccessDeserializer::new(map); + Tracks::deserialize(de)?.content + }; + // FIXME: Should we check if nodes.len() == count here? + return Ok(PlaylistGenericNode::Playlist(PlaylistPlaylistNode { + name: n.to_string(), + keytype: t.to_string(), + tracks, + })); + } + } + Some(unknown) => { + return Err(A::Error::unknown_variant(unknown, &["0", "1"])) + } + None => (), + } + } + + match node_type.as_deref() { + Some("0") => { + if name.is_none() { + Err(A::Error::missing_field("@Name")) + } else { + Err(A::Error::missing_field("@Count")) + } + } + Some("1") => { + if name.is_none() { + Err(A::Error::missing_field("@Name")) + } else if entries.is_none() { + Err(A::Error::missing_field("@Entries")) + } else { + Err(A::Error::missing_field("@KeyType")) + } + } + _ => Err(A::Error::missing_field("@Type")), + } + } + } + + deserializer.deserialize_map(PlaylistGenericNodeVisitor) + } +} + +#[derive(Debug, PartialEq, Clone, Deserialize)] +struct PlaylistFolderNode { + /// Name of NODE + #[serde(rename = "@Name")] + name: String, + // The "Count" attribute that contains the "Number of NODE in NODE" is omitted here, because we + // can just take the number of elements in the `tracks` vector instead. + /// Nodes + #[serde(rename = "NODE")] + nodes: Vec, +} + +impl Serialize for PlaylistFolderNode { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + struct Value<'a> { + /// Name of NODE + #[serde(rename = "@Name")] + name: &'a String, + /// Count + #[serde(rename = "@Count")] + count: usize, + /// Nodes + #[serde(rename = "NODE")] + nodes: &'a Vec, + } + + let value = Value { + name: &self.name, + count: self.nodes.len(), + nodes: &self.nodes, + }; + + value.serialize(serializer) + } +} + +#[derive(Debug, PartialEq, Clone, Deserialize)] +struct PlaylistPlaylistNode { + /// Name of NODE + #[serde(rename = "@Name")] + name: String, + // The "Entries" attribute that contains the "Number of TRACK in PLAYLIST" is omitted here, + // because we can just take the number of elements in the `tracks` vector instead. + /// Kind of identification + /// "0" (Track ID) or "1"(Location) + #[serde(rename = "@KeyType")] + keytype: String, + #[serde(rename = "TRACK")] + tracks: Vec, +} + +impl Serialize for PlaylistPlaylistNode { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + struct Value<'a> { + /// Name of NODE + #[serde(rename = "@Name")] + name: &'a String, + /// Number of TRACK in PLAYLIST + #[serde(rename = "@Entries")] + entries: usize, + /// Kind of identification + /// "0" (Track ID) or "1"(Location) + #[serde(rename = "@KeyType")] + keytype: &'a String, + #[serde(rename = "TRACK")] + tracks: &'a Vec, + } + + let value = Value { + name: &self.name, + entries: self.tracks.len(), + keytype: &self.keytype, + tracks: &self.tracks, + }; + + value.serialize(serializer) + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +struct PlaylistTrack { + /// Identification of track + /// "Track ID" or "Location" in "COLLECTION" + #[serde(rename = "@Key")] + key: i32, +} diff --git a/tests/test_loader.rs b/tests/test_loader.rs index ca6dbc2e..2b9ea9f7 100644 --- a/tests/test_loader.rs +++ b/tests/test_loader.rs @@ -10,3 +10,4 @@ include!(concat!(env!("OUT_DIR"), "/tests_pdb.rs")); include!(concat!(env!("OUT_DIR"), "/tests_anlz.rs")); include!(concat!(env!("OUT_DIR"), "/tests_setting.rs")); +include!(concat!(env!("OUT_DIR"), "/tests_xml.rs")); diff --git a/tests/tests_xml.rs.in b/tests/tests_xml.rs.in new file mode 100644 index 00000000..b0f4129a --- /dev/null +++ b/tests/tests_xml.rs.in @@ -0,0 +1,10 @@ +#[test] +#[allow(non_snake_case)] +fn xml_{name}() {{ + println!("Parsing file: {filepath}"); + let original_data = include_str!("{filepath}"); + let document: Document = quick_xml::de::from_str(original_data).expect("failed to deserialize XML"); + let serialized_data = quick_xml::se::to_string(&document).expect("failed to serialize XML"); + let document_after_roundtrip: Document = quick_xml::de::from_str(&serialized_data).expect("failed to deserialize XML"); + assert_eq!(document, document_after_roundtrip); +}}