i know kung fu
This commit is contained in:
1 parent
f81bffd339
commit
c422878493
3 files changed
+176
-68
No files matched your search
+1
-1
@@ -5,7 +5,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# rekordcrate = { git = "ssh://git@github.com/ntwl-xyz/rekordcrate.git", branch = "db" }
|
# rekordcrate = { git = "ssh://git@github.com/ntwl-xyz/rekordcrate.git", branch = "db" }
|
||||||
rekordcrate = { path = "../../rekordcrate" }
|
rekordcrate = { path = "../../code/rekordcrate" }
|
||||||
lofty = "0.21"
|
lofty = "0.21"
|
||||||
walkdir = "2.4"
|
walkdir = "2.4"
|
||||||
binrw = "0.15"
|
binrw = "0.15"
|
||||||
|
|||||||
+169
-67
@@ -1,10 +1,30 @@
|
|||||||
|
use rekordcrate::pdb::Artwork;
|
||||||
|
use rekordcrate::pdb::ArtworkId;
|
||||||
|
use rekordcrate::pdb::Page;
|
||||||
|
use rekordcrate::pdb::PageType;
|
||||||
|
use rekordcrate::pdb::PageIndex;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::io::{Seek, SeekFrom};
|
||||||
|
use binrw::BinWrite;
|
||||||
|
use rekordcrate::pdb::LabelId;
|
||||||
|
use rekordcrate::pdb::KeyId;
|
||||||
|
use rekordcrate::pdb::GenreId;
|
||||||
|
use rekordcrate::pdb::ArtistId;
|
||||||
|
use rekordcrate::pdb::AlbumId;
|
||||||
|
use rekordcrate::pdb::Album;
|
||||||
|
use rekordcrate::pdb::Artist;
|
||||||
|
use rekordcrate::pdb::Genre;
|
||||||
|
use rekordcrate::pdb::Key;
|
||||||
|
use rekordcrate::pdb::Label;
|
||||||
|
use rekordcrate::pdb::Table;
|
||||||
|
use std::fs::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use binrw::BinRead;
|
use binrw::BinRead;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use binrw::Endian;
|
use binrw::Endian;
|
||||||
use rekordcrate::pdb::{
|
use rekordcrate::pdb::{
|
||||||
Header, PageType, Row,
|
Header, Row,
|
||||||
Track, TrackId,
|
Track, TrackId,
|
||||||
PlaylistTreeNode, PlaylistTreeNodeId,
|
PlaylistTreeNode, PlaylistTreeNodeId,
|
||||||
PlaylistEntry,
|
PlaylistEntry,
|
||||||
@@ -21,17 +41,149 @@ pub enum DbError {
|
|||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub header: Header,
|
pub header: Header,
|
||||||
pub tracks: BTreeMap<TrackId, Track>,
|
pub tracks: BTreeMap<TrackId, Track>,
|
||||||
|
pub albums: BTreeMap<AlbumId, Album>,
|
||||||
|
pub artists: BTreeMap<ArtistId, Artist>,
|
||||||
|
pub artwork: BTreeMap<ArtworkId, Artwork>,
|
||||||
|
pub genres: BTreeMap<GenreId, Genre>,
|
||||||
|
pub keys: BTreeMap<KeyId, Key>,
|
||||||
|
pub labels: BTreeMap<LabelId, Label>,
|
||||||
pub nodes: BTreeMap<PlaylistTreeNodeId, PlaylistTreeNode>,
|
pub nodes: BTreeMap<PlaylistTreeNodeId, PlaylistTreeNode>,
|
||||||
// Keep playlist entries ordered by entry_index
|
// Keep playlist entries ordered by entry_index
|
||||||
pub entries: BTreeMap<PlaylistTreeNodeId, Vec<PlaylistEntry>>,
|
pub entries: BTreeMap<PlaylistTreeNodeId, Vec<PlaylistEntry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE: u32 = 4096;
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
|
pub fn export(self, path: &Path) -> std::io::Result<()> {
|
||||||
|
// 1) Reserve header page
|
||||||
|
let mut f = File::create(path)?;
|
||||||
|
f.write_all(&vec![0u8; PAGE_SIZE as usize])?;
|
||||||
|
|
||||||
|
// 2) Emit tables linearly, collect table metadata
|
||||||
|
let mut next_page_index: u32 = 1;
|
||||||
|
|
||||||
|
// ok now we're getting somewhere
|
||||||
|
// smash rows into row_groups of 16
|
||||||
|
// try to write those into a page until it is full
|
||||||
|
// yield pages until all the rows are in
|
||||||
|
// set the indices
|
||||||
|
// 420 blaze it
|
||||||
|
|
||||||
|
let (tracks_first, tracks_last) =
|
||||||
|
Self::write_table(&mut f, PageType::Tracks, next_page_index, self.tracks.values().cloned().map(Row::Track))?;
|
||||||
|
next_page_index = tracks_last + 1;
|
||||||
|
|
||||||
|
let (tree_first, tree_last) =
|
||||||
|
Self::write_table(&mut f, PageType::PlaylistTree, next_page_index, self.nodes.values().cloned().map(Row::PlaylistTreeNode))?;
|
||||||
|
next_page_index = tree_last + 1;
|
||||||
|
|
||||||
|
let plist_entries = self.entries.iter().flat_map(|(_pid, vec)| {
|
||||||
|
// ensure contiguous entry_index
|
||||||
|
let mut v = vec.clone();
|
||||||
|
v.sort_by_key(|e| e.entry_index);
|
||||||
|
v.into_iter().map(Row::PlaylistEntry)
|
||||||
|
});
|
||||||
|
let (entries_first, entries_last) =
|
||||||
|
Self::write_table(&mut f, PageType::PlaylistEntries, next_page_index, plist_entries)?;
|
||||||
|
next_page_index = entries_last + 1;
|
||||||
|
|
||||||
|
let (art_first, art_last) =
|
||||||
|
Self::write_table(&mut f, PageType::Artwork, next_page_index, self.artwork.values().cloned().map(Row::Artwork))?;
|
||||||
|
next_page_index = art_last + 1;
|
||||||
|
|
||||||
|
// 3) Build header and back-patch at page 0
|
||||||
|
let header = Header {
|
||||||
|
// fill other required fields in your crate
|
||||||
|
sequence: 1, // or previous+1
|
||||||
|
next_unused_page: PageIndex(next_page_index),
|
||||||
|
tables: vec![
|
||||||
|
Table { page_type: PageType::Tracks, first_page: PageIndex(tracks_first), last_page: PageIndex(tracks_last), empty_candidate: 0, /* … */ },
|
||||||
|
Table { page_type: PageType::PlaylistTree, first_page: PageIndex(tree_first), last_page: PageIndex(tree_last), empty_candidate: 0, /* … */ },
|
||||||
|
Table { page_type: PageType::PlaylistEntries, first_page: PageIndex(entries_first), last_page: PageIndex(entries_last), empty_candidate: 0, /* … */ },
|
||||||
|
Table { page_type: PageType::Artwork, first_page: PageIndex(art_first), last_page: PageIndex(art_last), empty_candidate: 0, /* … */ },
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write header at offset 0 and pad to one page
|
||||||
|
f.seek(SeekFrom::Start(0))?;
|
||||||
|
header.write(&mut f);
|
||||||
|
// pad residual of page 0 if BinWrite didn’t fill it:
|
||||||
|
let pos = f.stream_position()?;
|
||||||
|
if pos < PAGE_SIZE as u64 {
|
||||||
|
f.write_all(&vec![0u8; (PAGE_SIZE as u64 - pos) as usize])?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_table<I>(
|
||||||
|
f: &mut File,
|
||||||
|
page_type: PageType,
|
||||||
|
start_index: u32,
|
||||||
|
rows: I,
|
||||||
|
) -> std::io::Result<(u32, u32)>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Row>,
|
||||||
|
{
|
||||||
|
let mut page_index = start_index;
|
||||||
|
let mut first = 0;
|
||||||
|
let mut last = 0;
|
||||||
|
|
||||||
|
// Pack rows into pages. Use your crate’s Page builder to handle row groups.
|
||||||
|
let mut packer = PagePacker::new(page_type, PAGE_SIZE);
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
if !packer.try_push(&row) {
|
||||||
|
// flush full page
|
||||||
|
let mut page = packer.finish(page_index, /*next_page=*/0);
|
||||||
|
// Temporarily set next_page=0; we will fix it after writing the next page.
|
||||||
|
Self::write_page(f, &mut page)?;
|
||||||
|
if first == 0 { first = page_index; }
|
||||||
|
last = page_index;
|
||||||
|
page_index += 1;
|
||||||
|
// start new page with the row that didn’t fit
|
||||||
|
packer = PagePacker::new(page_type, PAGE_SIZE);
|
||||||
|
assert!(packer.try_push(&row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush final page if it has content or if this is an empty table create one empty page
|
||||||
|
if packer.is_empty() && first == 0 {
|
||||||
|
let mut page = Page::empty(page_type, page_index, /*next_page=*/0, PAGE_SIZE);
|
||||||
|
Self::write_page(f, &mut page)?;
|
||||||
|
first = page_index;
|
||||||
|
last = page_index;
|
||||||
|
} else {
|
||||||
|
let mut page = packer.finish(page_index, /*next_page=*/0);
|
||||||
|
Self::write_page(f, &mut page)?;
|
||||||
|
if first == 0 { first = page_index; }
|
||||||
|
last = page_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix `next_page` pointers by rewriting page headers if your Page writer requires it.
|
||||||
|
// Simpler: during emission, remember the previous page file offset and seek back
|
||||||
|
// to patch its `next_page` to `page_index`.
|
||||||
|
|
||||||
|
Ok((first, last))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_page(f: &mut File, page: &mut Page) -> std::io::Result<()> {
|
||||||
|
page.write(f);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_from_pdb(path: impl AsRef<Path>) -> rekordcrate::Result<Self> {
|
pub fn load_from_pdb(path: impl AsRef<Path>) -> rekordcrate::Result<Self> {
|
||||||
let mut f = std::fs::File::open(path)?;
|
let mut f = std::fs::File::open(path)?;
|
||||||
let header = Header::read(&mut f)?; // uses the type’s endianness
|
let header = Header::read(&mut f)?; // uses the type’s endianness
|
||||||
|
|
||||||
let mut tracks: BTreeMap<TrackId, Track> = BTreeMap::new();
|
let mut tracks: BTreeMap<TrackId, Track> = BTreeMap::new();
|
||||||
|
let mut albums: BTreeMap<AlbumId, Album> = BTreeMap::new();
|
||||||
|
let mut artists: BTreeMap<ArtistId, Artist> = BTreeMap::new();
|
||||||
|
let mut genres: BTreeMap<GenreId, Genre> = BTreeMap::new();
|
||||||
|
let mut keys: BTreeMap<KeyId, Key> = BTreeMap::new();
|
||||||
|
let mut labels: BTreeMap<LabelId, Label> = BTreeMap::new();
|
||||||
|
|
||||||
let mut nodes: BTreeMap<PlaylistTreeNodeId, PlaylistTreeNode> = BTreeMap::new();
|
let mut nodes: BTreeMap<PlaylistTreeNodeId, PlaylistTreeNode> = BTreeMap::new();
|
||||||
let mut entries: BTreeMap<PlaylistTreeNodeId, Vec<PlaylistEntry>> = BTreeMap::new();
|
let mut entries: BTreeMap<PlaylistTreeNodeId, Vec<PlaylistEntry>> = BTreeMap::new();
|
||||||
|
|
||||||
@@ -45,6 +197,21 @@ impl Database {
|
|||||||
Row::Track(t) => {
|
Row::Track(t) => {
|
||||||
tracks.insert(t.id, t.clone());
|
tracks.insert(t.id, t.clone());
|
||||||
}
|
}
|
||||||
|
Row::Artist(a) => {
|
||||||
|
artists.insert(a.id, a.clone());
|
||||||
|
}
|
||||||
|
Row::Album(a) => {
|
||||||
|
albums.insert(a.id, a.clone());
|
||||||
|
}
|
||||||
|
Row::Genre(g) => {
|
||||||
|
genres.insert(g.id, g.clone());
|
||||||
|
}
|
||||||
|
Row::Key(k) => {
|
||||||
|
keys.insert(k.id, k.clone());
|
||||||
|
}
|
||||||
|
Row::Label(l) => {
|
||||||
|
labels.insert(l.id, l.clone());
|
||||||
|
}
|
||||||
Row::PlaylistTreeNode(n) => {
|
Row::PlaylistTreeNode(n) => {
|
||||||
nodes.insert(n.id, n.clone());
|
nodes.insert(n.id, n.clone());
|
||||||
}
|
}
|
||||||
@@ -63,71 +230,6 @@ impl Database {
|
|||||||
v.sort_by_key(|e| e.entry_index); // needs `pub entry_index`
|
v.sort_by_key(|e| e.entry_index); // needs `pub entry_index`
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { header, tracks, nodes, entries })
|
Ok(Self { header, tracks, albums, artists, genres, keys, labels, 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
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,8 @@ use rekordcrate::pdb::Table;
|
|||||||
use rekordcrate::pdb::{Header, PlaylistTreeNode, PlaylistTreeNodeId};
|
use rekordcrate::pdb::{Header, PlaylistTreeNode, PlaylistTreeNodeId};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::fs::create_dir;
|
||||||
|
use std::fs::create_dir_all;
|
||||||
use std::{fs::exists, path::Path};
|
use std::{fs::exists, path::Path};
|
||||||
use db::Database;
|
use db::Database;
|
||||||
|
|
||||||
@@ -37,6 +39,8 @@ fn main() -> rekordcrate::Result<()> {
|
|||||||
// None => println!("wasnt there"),
|
// None => println!("wasnt there"),
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
create_dir_all("./data/PIONEER")?;
|
||||||
|
|
||||||
assert!(env::set_current_dir(&Path::new("./data")).is_ok());
|
assert!(env::set_current_dir(&Path::new("./data")).is_ok());
|
||||||
|
|
||||||
// strip the leading slash of the path to make it relative
|
// strip the leading slash of the path to make it relative
|
||||||
@@ -102,6 +106,8 @@ fn main() -> rekordcrate::Result<()> {
|
|||||||
|
|
||||||
println!("export={:?}", export);
|
println!("export={:?}", export);
|
||||||
|
|
||||||
|
db.header.tables.iter().for_each(|t| println!("{:?}", t.page_type));
|
||||||
|
|
||||||
// write header
|
// write header
|
||||||
// write page
|
// write page
|
||||||
// write row_groups
|
// write row_groups
|
||||||
|
|||||||
Reference in new issue
Block a user