From 3b9bcb1169de93d6b5e791e26f472df69edbf1f5 Mon Sep 17 00:00:00 2001 From: Nathan Rashleigh Date: Thu, 18 Sep 2025 21:16:10 +1000 Subject: [PATCH] link --- justfile | 3 + maple-py/link.py | 40 ++++++++ maple-rs/src/db.rs | 226 ++++++++++++++++++++++-------------------- maple-rs/src/main.rs | 186 +++++++++++++++++----------------- maple.sublime-project | 15 +-- 5 files changed, 264 insertions(+), 206 deletions(-) create mode 100644 maple-py/link.py diff --git a/justfile b/justfile index a9d6a5a..6627fc0 100644 --- a/justfile +++ b/justfile @@ -25,3 +25,6 @@ dl trg: docker compose -f compose.yaml run \ --remove-orphans \ maple -- sldl {{trg}} + +link plist dir: + python3 maple-py/link.py plist dir diff --git a/maple-py/link.py b/maple-py/link.py new file mode 100644 index 0000000..90e7cf4 --- /dev/null +++ b/maple-py/link.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import os, sys, pathlib + +m3u8 = pathlib.Path(sys.argv[1]).resolve() +outdir = pathlib.Path(sys.argv[2]).resolve() +outdir.mkdir(parents=True, exist_ok=True) +pl_dir = m3u8.parent + +def next_free(p: pathlib.Path) -> pathlib.Path: + if not p.exists(): + return p + stem, suf = p.stem, p.suffix + i = 1 + while True: + cand = p.with_name(f"{stem}-{i}{suf}") + if not cand.exists(): + return cand + i += 1 + +with m3u8.open("r", encoding="utf-8") as f: + for line in f: + s = line.strip() + if not s or s.startswith("#"): + continue + src = pathlib.Path(s) + if not src.is_absolute(): + src = (pl_dir / src).resolve() + else: + src = src.resolve() + + if not src.exists(): + print(f"skip (missing): {src}", file=sys.stderr) + continue + + dest = next_free(outdir / src.name) + try: + dest.symlink_to(src) + except OSError: + # fallback to hardlink if symlink not permitted + os.link(src, dest) diff --git a/maple-rs/src/db.rs b/maple-rs/src/db.rs index 73a6d8e..7179e7e 100644 --- a/maple-rs/src/db.rs +++ b/maple-rs/src/db.rs @@ -1,3 +1,4 @@ +use rekordcrate::pdb::RowGroup; use rekordcrate::pdb::Artwork; use rekordcrate::pdb::ArtworkId; use rekordcrate::pdb::Page; @@ -55,123 +56,129 @@ pub struct Database { const PAGE_SIZE: u32 = 4096; 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])?; - } + pub fn export(self, path: &Path) -> Result<(), binrw::Error> { + let pages = Page::from_rows(PageType::Tracks, 1, PAGE_SIZE, self.tracks.values().cloned().map(Row::Track))?; + println!("{:?}", pages); Ok(()) } + // 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])?; - fn write_table( - f: &mut File, - page_type: PageType, - start_index: u32, - rows: I, - ) -> std::io::Result<(u32, u32)> - where - I: IntoIterator, - { - let mut page_index = start_index; - let mut first = 0; - let mut last = 0; + // // 2) Emit tables linearly, collect table metadata + // let mut next_page_index: u32 = 1; - // Pack rows into pages. Use your crate’s Page builder to handle row groups. - let mut packer = PagePacker::new(page_type, PAGE_SIZE); + // // 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 - 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)); - } - } + // 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; - // 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; - } + // 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; - // 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`. + // 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; - Ok((first, last)) - } + // 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; - fn write_page(f: &mut File, page: &mut Page) -> std::io::Result<()> { - page.write(f); - Ok(()) - } + // // 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( + // f: &mut File, + // page_type: PageType, + // start_index: u32, + // rows: I, + // ) -> std::io::Result<(u32, u32)> + // where + // I: IntoIterator, + // { + // 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) -> rekordcrate::Result { let mut f = std::fs::File::open(path)?; @@ -180,6 +187,7 @@ impl Database { let mut tracks: BTreeMap = BTreeMap::new(); let mut albums: BTreeMap = BTreeMap::new(); let mut artists: BTreeMap = BTreeMap::new(); + let mut artwork: BTreeMap = BTreeMap::new(); let mut genres: BTreeMap = BTreeMap::new(); let mut keys: BTreeMap = BTreeMap::new(); let mut labels: BTreeMap = BTreeMap::new(); @@ -230,6 +238,6 @@ impl Database { v.sort_by_key(|e| e.entry_index); // needs `pub entry_index` } - Ok(Self { header, tracks, albums, artists, genres, keys, labels, nodes, entries }) + Ok(Self { header, tracks, albums, artists, artwork, genres, keys, labels, nodes, entries }) } } diff --git a/maple-rs/src/main.rs b/maple-rs/src/main.rs index 58688d8..4f8d76d 100644 --- a/maple-rs/src/main.rs +++ b/maple-rs/src/main.rs @@ -22,115 +22,119 @@ use db::Database; fn main() -> rekordcrate::Result<()> { let db = Database::load_from_pdb("ntr.pdb")?; println!("tracks: {}, playlists: {}", db.tracks.len(), db.nodes.len()); + println!("tables: {:?}", db.header); + println!("tables: {:?}", db.header.tables); - for n in db.nodes { - println!("{:?}", n); - } + db.export(&Path::new("./data/output.pdb"))?; - let (_first_id, first) = db.tracks.first_key_value().unwrap(); - let (_last_id, last) = db.tracks.last_key_value().unwrap(); - - println!("{:?}", first); - println!("{:?}", last); - - // let pic = db.tracks.iter().find(|(_id, t)| t.artwork_id == ArtworkId(680)); - // match pic { - // Some(it) => println!("{:?}", it), - // None => println!("wasnt there"), + // for n in db.nodes { + // println!("{:?}", n); // } - create_dir_all("./data/PIONEER")?; + // let (_first_id, first) = db.tracks.first_key_value().unwrap(); + // let (_last_id, last) = db.tracks.last_key_value().unwrap(); - assert!(env::set_current_dir(&Path::new("./data")).is_ok()); + // println!("{:?}", first); + // println!("{:?}", last); - // strip the leading slash of the path to make it relative - let anlz = first.analyze_path.clone().into_string()?; - let anlz = anlz.strip_prefix("/").unwrap(); - let mp3 = first.file_path.clone().into_string()?; - let mp3 = mp3.strip_prefix("/").unwrap(); + // // let pic = db.tracks.iter().find(|(_id, t)| t.artwork_id == ArtworkId(680)); + // // match pic { + // // Some(it) => println!("{:?}", it), + // // None => println!("wasnt there"), + // // } - println!("mp3={:?} exists={:?}", mp3, exists(mp3)); - println!("anlz={:?} exists={:?}", anlz, exists(anlz)); + // create_dir_all("./data/PIONEER")?; - let filename = "a680_m.jpg"; + // assert!(env::set_current_dir(&Path::new("./data")).is_ok()); - for entry in WalkDir::new(".") // start from current directory - .into_iter() - .filter_map(Result::ok) - .filter(|e| e.file_type().is_file()) - { - if entry.file_name() == filename { - println!("Found: {}", entry.path().display()); - } - } + // // strip the leading slash of the path to make it relative + // let anlz = first.analyze_path.clone().into_string()?; + // let anlz = anlz.strip_prefix("/").unwrap(); + // let mp3 = first.file_path.clone().into_string()?; + // let mp3 = mp3.strip_prefix("/").unwrap(); + + // println!("mp3={:?} exists={:?}", mp3, exists(mp3)); + // println!("anlz={:?} exists={:?}", anlz, exists(anlz)); + + // let filename = "a680_m.jpg"; + + // for entry in WalkDir::new(".") // start from current directory + // .into_iter() + // .filter_map(Result::ok) + // .filter(|e| e.file_type().is_file()) + // { + // if entry.file_name() == filename { + // println!("Found: {}", entry.path().display()); + // } + // } - let first_table = db.header.tables.first(); - println!("first_table={:?}", first_table); + // let first_table = db.header.tables.first(); + // println!("first_table={:?}", first_table); + + // let mut export = Header::default(); + // // let tracks = Table { page_type: PageType::Tracks, first_page: 0} + // // println!("export={:?}", export); + + // let track_table = Table { + // page_type: PageType::Tracks, + // first_page: PageIndex(1), + // last_page: PageIndex(1), + // empty_candidate: 2, + // }; + + // let tracks = vec![first, last]; + + // // let track_page = Page { + // // page_index: PageIndex(1), + // // page_type: PageType::Tracks, + // // next_page: PageIndex(2), + // // unknown1: 12, + // // unknown2: 0, + // // num_rows_small: 5, + // // unknown3: 160, + // // unknown4: 0, + // // page_flags: PageFlags(36), + // // free_size: 2302, + // // used_size: 1740, + // // unknown5: 1, + // // num_rows_large: 4, + // // unknown6: 0, + // // unknown7: 0, + // // row_groups: vec![row_groups], + // // }; + // let mut row_group = RowGroup::default(); + + // println!("track_table={:?}", track_table); + // export.tables.push(track_table); - let mut export = Header::default(); - // let tracks = Table { page_type: PageType::Tracks, first_page: 0} // println!("export={:?}", export); - let track_table = Table { - page_type: PageType::Tracks, - first_page: PageIndex(1), - last_page: PageIndex(1), - empty_candidate: 2, - }; + // db.header.tables.iter().for_each(|t| println!("{:?}", t.page_type)); - let tracks = vec![first, last]; + // // write header + // // write page + // // write row_groups - // let track_page = Page { - // page_index: PageIndex(1), - // page_type: PageType::Tracks, - // next_page: PageIndex(2), - // unknown1: 12, - // unknown2: 0, - // num_rows_small: 5, - // unknown3: 160, - // unknown4: 0, - // page_flags: PageFlags(36), - // free_size: 2302, - // used_size: 1740, - // unknown5: 1, - // num_rows_large: 4, - // unknown6: 0, - // unknown7: 0, - // row_groups: vec![row_groups], - // }; - let mut row_group = RowGroup::default(); + // // // Add tables to header + // // export.tables.push(track_table); + // // export.tables.push(playlist_tree_table); + // // export.tables.push(playlist_entries_table); + // // export.tables.push(artwork_table); - println!("track_table={:?}", track_table); - export.tables.push(track_table); + // // println!("Created PDB header with {} tables:", export.tables.len()); + // // println!(" - {} tracks", db.tracks.len()); + // // println!(" - {} playlist nodes", db.nodes.len()); + // // println!(" - {} playlist entries", db.entries.values().map(|v| v.len()).sum::()); - println!("export={:?}", export); - - db.header.tables.iter().for_each(|t| println!("{:?}", t.page_type)); - - // write header - // write page - // write row_groups - - // // Add tables to header - // export.tables.push(track_table); - // export.tables.push(playlist_tree_table); - // export.tables.push(playlist_entries_table); - // export.tables.push(artwork_table); - - // println!("Created PDB header with {} tables:", export.tables.len()); - // println!(" - {} tracks", db.tracks.len()); - // println!(" - {} playlist nodes", db.nodes.len()); - // println!(" - {} playlist entries", db.entries.values().map(|v| v.len()).sum::()); - - // for (i, table) in export.tables.iter().enumerate() { - // println!(" Table {}: {:?} (pages {}-{})", - // i + 1, - // table.page_type, - // table.first_page.0, - // table.last_page.0 - // ); - // } + // // for (i, table) in export.tables.iter().enumerate() { + // // println!(" Table {}: {:?} (pages {}-{})", + // // i + 1, + // // table.page_type, + // // table.first_page.0, + // // table.last_page.0 + // // ); + // // } Ok(()) } diff --git a/maple.sublime-project b/maple.sublime-project index 44ad785..42b36c7 100755 --- a/maple.sublime-project +++ b/maple.sublime-project @@ -10,6 +10,9 @@ { "path": "C:\\Users\\ntr\\code\\rekordcrate", }, + { + "path": "/home/ntr/code/rekordcrate" + }, ], "settings": { @@ -23,11 +26,11 @@ { "run": { - "target": "--bin maple" - } - } - } - } - } + "target": "--bin maple", + }, + }, + }, + }, + }, }, }