From 76bf94823b314b61195e1c216d07d33f617c2297 Mon Sep 17 00:00:00 2001 From: Nathan Rashleigh Date: Sun, 7 Sep 2025 17:29:37 +1000 Subject: [PATCH] rusting --- .gitignore | 2 +- Cargo.lock => maple-rs/Cargo.lock | 122 ++++++++++++++++++++- Cargo.toml => maple-rs/Cargo.toml | 9 +- src/main.rs => maple-rs/src/library.rs | 0 maple-rs/src/playlists.rs | 146 +++++++++++++++++++++++++ maple.sublime-project | 9 +- src/playlists.rs | 25 ----- 7 files changed, 281 insertions(+), 32 deletions(-) rename Cargo.lock => maple-rs/Cargo.lock (87%) rename Cargo.toml => maple-rs/Cargo.toml (57%) rename src/main.rs => maple-rs/src/library.rs (100%) create mode 100644 maple-rs/src/playlists.rs delete mode 100644 src/playlists.rs diff --git a/.gitignore b/.gitignore index eff8c52..9c16920 100755 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,6 @@ surf.sublime-workspace # Added by cargo -/target +**/target maple.sublime-workspace *.secret.yaml diff --git a/Cargo.lock b/maple-rs/Cargo.lock similarity index 87% rename from Cargo.lock rename to maple-rs/Cargo.lock index 043c79a..ef02b7c 100644 --- a/Cargo.lock +++ b/maple-rs/Cargo.lock @@ -94,6 +94,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base85" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36915bbaca237c626689b5bd14d02f2ba7a5a359d30a2a08be697392e3718079" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "binrw" version = "0.14.1" @@ -118,6 +127,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + [[package]] name = "bumpalo" version = "3.19.0" @@ -244,6 +259,18 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "flate2" version = "1.1.2" @@ -254,12 +281,36 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + [[package]] name = "heck" version = "0.5.0" @@ -312,6 +363,17 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lofty" version = "0.21.1" @@ -348,8 +410,12 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" name = "maple" version = "0.1.0" dependencies = [ + "base85", + "binrw", + "flate2", "lofty", "rekordcrate", + "rusqlite", "walkdir", ] @@ -456,6 +522,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "proc-macro2" version = "1.0.101" @@ -528,7 +600,21 @@ dependencies = [ "parse-display", "quick-xml", "serde", - "thiserror", + "thiserror 2.0.15", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", ] [[package]] @@ -572,6 +658,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "static_assertions" version = "1.1.0" @@ -629,13 +721,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.15", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -661,6 +773,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/maple-rs/Cargo.toml similarity index 57% rename from Cargo.toml rename to maple-rs/Cargo.toml index 97ccd34..449dae0 100644 --- a/Cargo.toml +++ b/maple-rs/Cargo.toml @@ -7,6 +7,13 @@ edition = "2024" rekordcrate = "0.3.0" lofty = "0.21" walkdir = "2.4" +binrw = "0.14.1" +base85 = "2.0.0" +flate2 = "1.1.2" + +[dependencies.rusqlite] +version = "0.37.0" +features = ["bundled-sqlcipher"] [[bin]] name = "playlists" @@ -14,4 +21,4 @@ path = "src/playlists.rs" [[bin]] name = "library" -path = "src/main.rs" +path = "src/library.rs" diff --git a/src/main.rs b/maple-rs/src/library.rs similarity index 100% rename from src/main.rs rename to maple-rs/src/library.rs diff --git a/maple-rs/src/playlists.rs b/maple-rs/src/playlists.rs new file mode 100644 index 0000000..4ee7b6c --- /dev/null +++ b/maple-rs/src/playlists.rs @@ -0,0 +1,146 @@ +use rusqlite::{params, Connection, Result}; +use std::io::Read; +use flate2::read::ZlibDecoder; +use base85; + +const BLOB_KEY: &[u8] = b"657f48f84c437cc1"; +const BLOB: &[u8] = b"PN_Pq^*N>(JYe*u^8;Yg76HuZ)b9;DpoTXV(6ItkU`}8*m6tx_I{Solh_N#dfe{v="; + +fn deobfuscate() -> Result> { + // 1. Base85 decode + let data = base85::decode(std::str::from_utf8(BLOB)?)?; + + // 2. XOR with key + let xored: Vec = data + .iter() + .enumerate() + .map(|(i, &b)| b ^ BLOB_KEY[i % BLOB_KEY.len()]) + .collect(); + + // 3. zlib decompress + let mut decoder = ZlibDecoder::new(&xored[..]); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed)?; + + // 4. UTF-8 decode + Ok(String::from_utf8(decompressed)?) +} + +// Structs for mapping rows +#[derive(Debug)] +struct Playlist { + id: i64, + name: String, +} + +#[derive(Debug)] +struct Track { + id: i64, + title: String, +} + +// List all playlists +fn list_playlists(conn: &Connection) -> Result> { + let mut stmt = conn.prepare("SELECT ID, Name FROM plPlaylist ORDER BY ID")?; + let playlists = stmt.query_map([], |row| { + Ok(Playlist { + id: row.get(0)?, + name: row.get(1)?, + }) + })?; + + Ok(playlists.filter_map(|x| x.ok()).collect()) +} + +// List tracks in a playlist +fn list_tracks_in_playlist(conn: &Connection, playlist_id: i64) -> Result> { + let mut stmt = conn.prepare( + "SELECT c.ID, c.Name + FROM plSongPlaylistRel r + JOIN plContent c ON r.SongID = c.ID + WHERE r.PlaylistID = ? + ORDER BY r.SortOrder", + )?; + let tracks = stmt.query_map([playlist_id], |row| { + Ok(Track { + id: row.get(0)?, + title: row.get(1)?, + }) + })?; + + Ok(tracks.filter_map(|x| x.ok()).collect()) +} + +// Add a track to a playlist +fn add_track_to_playlist(conn: &Connection, playlist_id: i64, track_id: i64) -> Result<()> { + // Get next ID for relation + let next_id: i64 = conn.query_row( + "SELECT IFNULL(MAX(ID), 0) + 1 FROM plSongPlaylistRel", + [], + |row| row.get(0), + )?; + + // Get next sort order for playlist + let next_sort: i64 = conn.query_row( + "SELECT IFNULL(MAX(SortOrder), 0) + 1 FROM plSongPlaylistRel WHERE PlaylistID = ?", + [playlist_id], + |row| row.get(0), + )?; + + conn.execute( + "INSERT INTO plSongPlaylistRel (ID, PlaylistID, SongID, SortOrder) + VALUES (?, ?, ?, ?)", + params![next_id, playlist_id, track_id, next_sort], + )?; + + Ok(()) +} + +// Remove a track from a playlist +fn remove_track_from_playlist(conn: &Connection, playlist_id: i64, track_id: i64) -> Result<()> { + conn.execute( + "DELETE FROM plSongPlaylistRel WHERE PlaylistID = ? AND SongID = ?", + params![playlist_id, track_id], + )?; + Ok(()) +} + +/// List all tables in a Rekordbox SQLite DB (master.db or export.pdb) +fn list_tables(conn: &Connection) -> Result> { + let mut stmt = conn.prepare( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;" + )?; + let rows = stmt.query_map([], |row| row.get(0))?; + + let mut tables = Vec::new(); + for r in rows { + if let Ok(name) = r { + tables.push(name); + } + } + Ok(tables) +} + +// Open Rekordbox DB (unencrypted). For Rekordbox 6+, needs SQLCipher build. +fn open_rekordbox_db(path: &str, key: String) -> Result { + let conn = Connection::open(path)?; + // conn.pragma_update(None, "key", key)?; + Ok(conn) +} + +fn main() -> Result<(), Box> { + let key = deobfuscate()?; + let conn = open_rekordbox_db("../data/PIONEER/rekordbox/export.pdb", key)?; + + list_tables(&conn)?; + + // // Example: list playlists + // for p in list_playlists(&conn)? { + // println!("Playlist {}: {}", p.id, p.name); + // } + + // // Example: add track 123 to playlist 10 + // add_track_to_playlist(&conn, 10, 123)?; + + Ok(()) +} diff --git a/maple.sublime-project b/maple.sublime-project index a026f0a..d516345 100755 --- a/maple.sublime-project +++ b/maple.sublime-project @@ -2,7 +2,10 @@ "folders": [ { - "path": "." - } - ] + "path": ".", + }, + { + "path": "/home/ntr/code/rekordcrate" + }, + ], } diff --git a/src/playlists.rs b/src/playlists.rs deleted file mode 100644 index ebd38eb..0000000 --- a/src/playlists.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::path::Path; - -fn main() { - let pdb_path = "data/PIONEER/rekordbox/export.pdb"; - - if !Path::new(pdb_path).exists() { - println!("Pioneer database {} not found", pdb_path); - return; - } - - match rekordcrate::parse_pdb(pdb_path) { - Ok(database) => { - println!("Playlists in {}:\n", pdb_path); - - for playlist in &database.playlists { - println!(" {}", playlist.name); - } - - println!("\nTotal playlists: {}", database.playlists.len()); - } - Err(e) => { - println!("Error reading Pioneer database: {}", e); - } - } -} \ No newline at end of file