diff --git a/maple-rs/Cargo.lock b/maple-rs/Cargo.lock index ef02b7c..ff1f935 100644 --- a/maple-rs/Cargo.lock +++ b/maple-rs/Cargo.lock @@ -94,20 +94,11 @@ 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" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b" +checksum = "81419ff39e6ed10a92a7f125290859776ced35d9a08a665ae40b23e7ca702f30" dependencies = [ "array-init", "binrw_derive", @@ -116,15 +107,15 @@ dependencies = [ [[package]] name = "binrw_derive" -version = "0.14.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ba42866ce5bced2645bfa15e97eef2c62d2bdb530510538de8dd3d04efff3c" +checksum = "376404e55ec40d0d6f8b4b7df3f87b87954bd987f0cf9a7207ea3b6ea5c9add4" dependencies = [ "either", "owo-colors", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -211,7 +202,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -397,7 +388,7 @@ checksum = "ed9983e64b2358522f745c1251924e3ab7252d55637e80f6a0a3de642d6a9efc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -410,9 +401,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" name = "maple" version = "0.1.0" dependencies = [ - "base85", "binrw", - "flate2", "lofty", "rekordcrate", "rusqlite", @@ -436,9 +425,9 @@ dependencies = [ [[package]] name = "modular-bitfield" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +checksum = "be0d5274763b5572c8f29dadb5cd0bc59de64c805de433c1b556075f733b0a1a" dependencies = [ "modular-bitfield-impl", "static_assertions", @@ -446,13 +435,13 @@ dependencies = [ [[package]] name = "modular-bitfield-impl" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +checksum = "f8eec4327f127d4d18c54c8bfbf7b05d74cc9a1befdcc6283a241238ffbc84c6" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -487,9 +476,9 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" [[package]] name = "parse-display" @@ -513,7 +502,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.106", + "syn", ] [[package]] @@ -539,9 +528,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", "serde", @@ -588,8 +577,6 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rekordcrate" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18888593c45eb477d87e28f6c226598bc1286d54a7f67d87cee25d73707bf915" dependencies = [ "binrw", "chrono", @@ -600,7 +587,7 @@ dependencies = [ "parse-display", "quick-xml", "serde", - "thiserror 2.0.15", + "thiserror", ] [[package]] @@ -649,7 +636,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -685,7 +672,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.106", + "syn", ] [[package]] @@ -696,18 +683,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "syn", ] [[package]] @@ -721,33 +697,13 @@ 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 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", + "thiserror-impl", ] [[package]] @@ -758,7 +714,7 @@ checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -811,7 +767,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn", "wasm-bindgen-shared", ] @@ -833,7 +789,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -877,7 +833,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -888,7 +844,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] diff --git a/maple-rs/Cargo.toml b/maple-rs/Cargo.toml index 449dae0..2162f7e 100644 --- a/maple-rs/Cargo.toml +++ b/maple-rs/Cargo.toml @@ -4,12 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] -rekordcrate = "0.3.0" +rekordcrate = { path = "/home/ntr/code/rekordcrate" } lofty = "0.21" walkdir = "2.4" -binrw = "0.14.1" -base85 = "2.0.0" -flate2 = "1.1.2" +binrw = "0.15" [dependencies.rusqlite] version = "0.37.0" diff --git a/maple-rs/src/playlists.rs b/maple-rs/src/playlists.rs index 4ee7b6c..af2019c 100644 --- a/maple-rs/src/playlists.rs +++ b/maple-rs/src/playlists.rs @@ -1,146 +1,75 @@ -use rusqlite::{params, Connection, Result}; -use std::io::Read; -use flate2::read::ZlibDecoder; -use base85; +use binrw::BinRead; +use std::path::PathBuf; +use rekordcrate::pdb::{Header, Row, PageType, PlaylistTreeNode, PlaylistTreeNodeId}; +use std::collections::HashMap; -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 list_playlists(path: &PathBuf) -> rekordcrate::Result<()> { -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); - } + fn print_children_of( + tree: &HashMap>, + id: PlaylistTreeNodeId, + level: usize, + ) { + tree.get(&id) + .iter() + .flat_map(|nodes| nodes.iter()) + .for_each(|node| { + println!( + "{}{} {}", + " ".repeat(level), + if node.is_folder() { "🗀" } else { "🗎" }, + node.name.clone().into_string().unwrap(), + ); + print_children_of(tree, node.id, level + 1); + }); } - Ok(tables) + + let mut reader = std::fs::File::open(path)?; + let header = Header::read(&mut reader)?; + + let mut tree: HashMap> = HashMap::new(); + + header + .tables + .iter() + .filter(|table| table.page_type == PageType::PlaylistTree) + .flat_map(|table| { + header + .read_pages( + &mut reader, + binrw::Endian::NATIVE, + (&table.first_page, &table.last_page), + ) + .unwrap() + .into_iter() + .flat_map(|page| page.row_groups.into_iter()) + .flat_map(|row_group| { + row_group + .present_rows() + .iter() + .map(|row| { + if let Row::PlaylistTreeNode(playlist_tree) = row { + playlist_tree.clone() + } else { + unreachable!("encountered non-playlist tree row in playlist table"); + } + }) + .collect::>() + .into_iter() + }) + }) + .for_each(|row| tree.entry(row.parent_id).or_default().push(row)); + + print_children_of(&tree, PlaylistTreeNodeId(0), 0); + + Ok(()) } -// 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)?; + let p = PathBuf::from("../data/PIONEER/rekordbox/export.pdb"); - 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)?; + list_playlists(&p)?; Ok(()) }