This commit is contained in:
Nathan Rashleigh committed 2025-09-10 23:03:25 +10:00
1 parent 66c5207ae1
commit 07017c5def
3 files changed
+153 -1

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
use std::path::Path;
use binrw::BinRead;
use std::collections::BTreeMap;
use binrw::Endian;
use rekordcrate::pdb::{
Header, PageType, Row,
Track, TrackId,
PlaylistTreeNode, PlaylistTreeNodeId,
PlaylistEntry,
};
#[derive(Debug)]
pub enum DbError {
UnknownPlaylist,
UnknownTrack,
PositionOutOfBounds,
}
#[derive(Debug)]
pub struct Database {
pub header: Header,
pub tracks: BTreeMap<TrackId, Track>,
pub nodes: BTreeMap<PlaylistTreeNodeId, PlaylistTreeNode>,
// Keep playlist entries ordered by entry_index
pub entries: BTreeMap<PlaylistTreeNodeId, Vec<PlaylistEntry>>,
}
impl Database {
pub fn load_from_pdb(path: impl AsRef<Path>) -> rekordcrate::Result<Self> {
let mut f = std::fs::File::open(path)?;
let header = Header::read(&mut f)?; // uses the type’s endianness
let mut tracks: BTreeMap<TrackId, Track> = BTreeMap::new();
let mut nodes: BTreeMap<PlaylistTreeNodeId, PlaylistTreeNode> = BTreeMap::new();
let mut entries: BTreeMap<PlaylistTreeNodeId, Vec<PlaylistEntry>> = BTreeMap::new();
for table in &header.tables {
let pages = header.read_pages(&mut f, Endian::NATIVE, (&table.first_page, &table.last_page))?;
for page in pages {
for rg in page.row_groups {
for row in rg.present_rows() {
match row {
Row::Track(t) => {
tracks.insert(t.id, t.clone());
}
Row::PlaylistTreeNode(n) => {
nodes.insert(n.id, n.clone());
}
Row::PlaylistEntry(e) => {
entries.entry(e.playlist_id).or_default().push(e.clone());
}
_ => {}
}
}
}
}
}
// Ensure deterministic ordering within each playlist
for v in entries.values_mut() {
v.sort_by_key(|e| e.entry_index); // needs `pub entry_index`
}
Ok(Self { header, tracks, nodes, entries })
}
/// Export to low-level tables.
pub fn into_tables(
self,
) -> (
Header,
Vec<Track>,
Vec<PlaylistTreeNode>,
Vec<(PlaylistTreeNodeId, PlaylistEntry)>,
) {
let nodes = self.nodes.into_values().collect();
let tracks = self.tracks.into_values().collect();
let mut flat_entries = Vec::new();
for (pid, vs) in self.entries {
for e in vs {
flat_entries.push((pid, e));
}
}
(self.header, tracks, nodes, flat_entries)
}
/// Add a ready-made entry (you build it).
pub fn add_entry(
&mut self,
playlist_id: PlaylistTreeNodeId,
entry: PlaylistEntry,
position: Option<usize>,
) -> Result<(), DbError> {
if !self.nodes.contains_key(&playlist_id) {
return Err(DbError::UnknownPlaylist);
}
let vec = self.entries.entry(playlist_id).or_default();
match position {
Some(i) if i <= vec.len() => vec.insert(i, entry),
Some(_) => return Err(DbError::PositionOutOfBounds),
None => vec.push(entry),
}
Ok(())
}
/// Convenience: you pass a track id and a constructor for `PlaylistEntry`.
pub fn add_track_to_playlist_with<E>(
&mut self,
playlist_id: PlaylistTreeNodeId,
track_id: TrackId,
position: Option<usize>,
make_entry: E,
) -> Result<(), DbError>
where
E: FnOnce(TrackId) -> PlaylistEntry,
{
if !self.nodes.contains_key(&playlist_id) {
return Err(DbError::UnknownPlaylist);
}
if !self.tracks.contains_key(&track_id) {
return Err(DbError::UnknownTrack);
}
let entry = make_entry(track_id);
self.add_entry(playlist_id, entry, position)
}
// fn alloc_node_id(&mut self) -> u32 {
// let id = self.next_node_id;
// self.next_node_id = self.next_node_id.saturating_add(1);
// id
// }
}
+15
View File
@@ -0,0 +1,15 @@
mod db;
use db::Database;
// Example
fn main() -> rekordcrate::Result<()> {
let db = Database::load_from_pdb("ntr.pdb")?;
println!("tracks: {}, playlists: {}", db.tracks.len(), db.nodes.len());
for n in db.nodes {
println!("{:?}", n);
}
Ok(())
}
+5 -1
View File
@@ -1,8 +1,12 @@
mod db;
use binrw::BinRead; use binrw::BinRead;
use std::path::PathBuf; use std::path::PathBuf;
use rekordcrate::pdb::{Header, Row, PageType, PlaylistTreeNode, PlaylistTreeNodeId}; use rekordcrate::pdb::{Header, Row, PageType, PlaylistTreeNode, PlaylistTreeNodeId};
use std::collections::HashMap; use std::collections::HashMap;
use db::Database;
fn list_playlists(path: &PathBuf) -> rekordcrate::Result<()> { fn list_playlists(path: &PathBuf) -> rekordcrate::Result<()> {
fn print_children_of( fn print_children_of(
@@ -67,7 +71,7 @@ fn list_playlists(path: &PathBuf) -> rekordcrate::Result<()> {
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let p = PathBuf::from("../data/PIONEER/rekordbox/export.pdb"); let p = PathBuf::from("./ntr.pdb");
list_playlists(&p)?; list_playlists(&p)?;