local rekordcrate

This commit is contained in:
Nathan Rashleigh committed 2025-09-10 21:31:27 +10:00
1 parent 76bf94823b
commit 66c5207ae1
3 files changed
+93 -210

No files matched your search

+27 -71
View File
@@ -94,20 +94,11 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 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]] [[package]]
name = "binrw" name = "binrw"
version = "0.14.1" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b" checksum = "81419ff39e6ed10a92a7f125290859776ced35d9a08a665ae40b23e7ca702f30"
dependencies = [ dependencies = [
"array-init", "array-init",
"binrw_derive", "binrw_derive",
@@ -116,15 +107,15 @@ dependencies = [
[[package]] [[package]]
name = "binrw_derive" name = "binrw_derive"
version = "0.14.1" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8ba42866ce5bced2645bfa15e97eef2c62d2bdb530510538de8dd3d04efff3c" checksum = "376404e55ec40d0d6f8b4b7df3f87b87954bd987f0cf9a7207ea3b6ea5c9add4"
dependencies = [ dependencies = [
"either", "either",
"owo-colors", "owo-colors",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn",
] ]
[[package]] [[package]]
@@ -211,7 +202,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@@ -397,7 +388,7 @@ checksum = "ed9983e64b2358522f745c1251924e3ab7252d55637e80f6a0a3de642d6a9efc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@@ -410,9 +401,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
name = "maple" name = "maple"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base85",
"binrw", "binrw",
"flate2",
"lofty", "lofty",
"rekordcrate", "rekordcrate",
"rusqlite", "rusqlite",
@@ -436,9 +425,9 @@ dependencies = [
[[package]] [[package]]
name = "modular-bitfield" name = "modular-bitfield"
version = "0.11.2" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" checksum = "be0d5274763b5572c8f29dadb5cd0bc59de64c805de433c1b556075f733b0a1a"
dependencies = [ dependencies = [
"modular-bitfield-impl", "modular-bitfield-impl",
"static_assertions", "static_assertions",
@@ -446,13 +435,13 @@ dependencies = [
[[package]] [[package]]
name = "modular-bitfield-impl" name = "modular-bitfield-impl"
version = "0.11.2" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" checksum = "f8eec4327f127d4d18c54c8bfbf7b05d74cc9a1befdcc6283a241238ffbc84c6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn",
] ]
[[package]] [[package]]
@@ -487,9 +476,9 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]] [[package]]
name = "owo-colors" name = "owo-colors"
version = "3.5.0" version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
[[package]] [[package]]
name = "parse-display" name = "parse-display"
@@ -513,7 +502,7 @@ dependencies = [
"regex", "regex",
"regex-syntax", "regex-syntax",
"structmeta", "structmeta",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@@ -539,9 +528,9 @@ dependencies = [
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.37.5" version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@@ -588,8 +577,6 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "rekordcrate" name = "rekordcrate"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18888593c45eb477d87e28f6c226598bc1286d54a7f67d87cee25d73707bf915"
dependencies = [ dependencies = [
"binrw", "binrw",
"chrono", "chrono",
@@ -600,7 +587,7 @@ dependencies = [
"parse-display", "parse-display",
"quick-xml", "quick-xml",
"serde", "serde",
"thiserror 2.0.15", "thiserror",
] ]
[[package]] [[package]]
@@ -649,7 +636,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@@ -685,7 +672,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@@ -696,18 +683,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
]
[[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",
] ]
[[package]] [[package]]
@@ -721,33 +697,13 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.15" version = "2.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850"
dependencies = [ dependencies = [
"thiserror-impl 2.0.15", "thiserror-impl",
]
[[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]] [[package]]
@@ -758,7 +714,7 @@ checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@@ -811,7 +767,7 @@ dependencies = [
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -833,7 +789,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -877,7 +833,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@@ -888,7 +844,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
+2 -4
View File
@@ -4,12 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
rekordcrate = "0.3.0" rekordcrate = { path = "/home/ntr/code/rekordcrate" }
lofty = "0.21" lofty = "0.21"
walkdir = "2.4" walkdir = "2.4"
binrw = "0.14.1" binrw = "0.15"
base85 = "2.0.0"
flate2 = "1.1.2"
[dependencies.rusqlite] [dependencies.rusqlite]
version = "0.37.0" version = "0.37.0"
+64 -135
View File
@@ -1,146 +1,75 @@
use rusqlite::{params, Connection, Result}; use binrw::BinRead;
use std::io::Read; use std::path::PathBuf;
use flate2::read::ZlibDecoder; use rekordcrate::pdb::{Header, Row, PageType, PlaylistTreeNode, PlaylistTreeNodeId};
use base85; use std::collections::HashMap;
const BLOB_KEY: &[u8] = b"657f48f84c437cc1"; fn list_playlists(path: &PathBuf) -> rekordcrate::Result<()> {
const BLOB: &[u8] = b"PN_Pq^*N>(JYe*u^8;Yg76HuZ<mR13S?=>)b9;DpoTXV(6ItkU`}8*m6tx_I{Solh_N#dfe{v=";
fn deobfuscate() -> Result<String, Box<dyn std::error::Error>> { fn print_children_of(
// 1. Base85 decode tree: &HashMap<PlaylistTreeNodeId, Vec<PlaylistTreeNode>>,
let data = base85::decode(std::str::from_utf8(BLOB)?)?; id: PlaylistTreeNodeId,
level: usize,
// 2. XOR with key ) {
let xored: Vec<u8> = data tree.get(&id)
.iter() .iter()
.enumerate() .flat_map(|nodes| nodes.iter())
.map(|(i, &b)| b ^ BLOB_KEY[i % BLOB_KEY.len()]) .for_each(|node| {
.collect(); println!(
"{}{} {}",
// 3. zlib decompress " ".repeat(level),
let mut decoder = ZlibDecoder::new(&xored[..]); if node.is_folder() { "🗀" } else { "🗎" },
let mut decompressed = Vec::new(); node.name.clone().into_string().unwrap(),
decoder.read_to_end(&mut decompressed)?; );
print_children_of(tree, node.id, level + 1);
// 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<Vec<Playlist>> {
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<Vec<Track>> {
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<Vec<String>> {
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)
let mut reader = std::fs::File::open(path)?;
let header = Header::read(&mut reader)?;
let mut tree: HashMap<PlaylistTreeNodeId, Vec<PlaylistTreeNode>> = 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::<Vec<PlaylistTreeNode>>()
.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<Connection> {
let conn = Connection::open(path)?;
// conn.pragma_update(None, "key", key)?;
Ok(conn)
}
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = deobfuscate()?; let p = PathBuf::from("../data/PIONEER/rekordbox/export.pdb");
let conn = open_rekordbox_db("../data/PIONEER/rekordbox/export.pdb", key)?;
list_tables(&conn)?; list_playlists(&p)?;
// // 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(()) Ok(())
} }