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

+64 -135
View File
@@ -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<mR13S?=>)b9;DpoTXV(6ItkU`}8*m6tx_I{Solh_N#dfe{v=";
fn list_playlists(path: &PathBuf) -> rekordcrate::Result<()> {
fn deobfuscate() -> Result<String, Box<dyn std::error::Error>> {
// 1. Base85 decode
let data = base85::decode(std::str::from_utf8(BLOB)?)?;
// 2. XOR with key
let xored: Vec<u8> = 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<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);
}
fn print_children_of(
tree: &HashMap<PlaylistTreeNodeId, Vec<PlaylistTreeNode>>,
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<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>> {
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(())
}