diff --git a/Cargo.lock b/Cargo.lock index 5527701..57586e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,7 +403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -975,8 +975,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "itunesdb" -version = "0.1.6" -source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#eac020520b7efa2173065772105ab0b2c4ba6da6" +version = "0.1.11" +source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#4204fdcda886438d815da7b70f736506e06a22e3" dependencies = [ "bincode", "env_logger", @@ -1081,7 +1081,6 @@ dependencies = [ "md-5", "puremp3", "ratatui", - "redb", "regex", "rusb", "serde", @@ -1395,15 +1394,6 @@ dependencies = [ "unicode-width 0.2.0", ] -[[package]] -name = "redb" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0a72cd7140de9fc3e318823b883abf819c20d478ec89ce880466dc2ef263c6" -dependencies = [ - "libc", -] - [[package]] name = "redox_syscall" version = "0.5.8" @@ -1538,7 +1528,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1881,7 +1871,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2339,7 +2329,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 204c44f..1eb7def 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ futures = "0.3" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7.12", features = ["codec"] } soundcloud = { version = "0.1.8", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } -itunesdb = { version = "0.1.6", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } -redb = "2.4.0" +itunesdb = { version = "0.1.11", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } md-5 = "0.10.6" puremp3 = "0.1.0" diff --git a/src/db.rs b/src/db.rs deleted file mode 100644 index de8bc72..0000000 --- a/src/db.rs +++ /dev/null @@ -1,235 +0,0 @@ -use std::fs::File; -use std::ops::Deref; -use itunesdb::xobjects::{XArgument, XPlaylist, XTrackItem}; -use md5::{Digest, Md5}; -use redb::{Database, Error, ReadableTable, TableDefinition}; -use serde::{Deserialize, Serialize}; -use soundcloud::sobjects::CloudTrack; - -use crate::config::{get_db, get_temp_dl_dir}; - -const TRACKS: TableDefinition> = TableDefinition::new("tracks"); -const PLAYLISTS: TableDefinition> = TableDefinition::new("playlists"); - -#[derive(Serialize, Deserialize)] -pub struct Track { - pub unique_id: u32, - filetype: u32, - stars: u8, - last_modified_time: u32, - size: u32, - pub length: u32, - year: u32, - pub bitrate: u32, - sample_rate: u32, - play_count: u32, - pub dbid: u64, - bpm: u16, - skip_count: u32, - has_artwork: u8, - media_type: u32, - pub title: String, - pub location: String, - album: String, - pub artist: String, - pub genre: String, -} - -#[derive(Serialize, Deserialize)] -pub struct DBPlaylist { - pub persistent_playlist_id: u64, - pub title: String, - pub timestamp: u32, - pub is_master: bool, - pub tracks: Vec, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct Playlist { - pub persistent_playlist_id: u64, - pub title: String, - pub timestamp: u32, - pub is_master: bool, - pub tracks: Vec, -} - -impl From for Track { - fn from(value: CloudTrack) -> Self { - let mut track_path = get_temp_dl_dir(); - track_path.push(value.id.to_string()); - track_path.set_extension("mp3"); - let f = File::open(&track_path).unwrap(); - let data = &std::fs::read(&track_path).unwrap()[..]; - let (header, _samples) = puremp3::read_mp3(data).unwrap(); - Track { - unique_id: 0, - filetype: 0, - stars: 0, - last_modified_time: 0, - size: f.metadata().unwrap().len() as u32, - length: 0, - year: 0, - bitrate: header.bitrate.bps() / 1000, - sample_rate: header.sample_rate.hz(), - play_count: 0, - dbid: hash(data), - bpm: 0, - skip_count: 0, - has_artwork: 0, - media_type: 0, - title: value.title.unwrap(), - location: String::new(), - album: String::new(), - artist: value - .user - .map_or(String::new(), |a| a.username.unwrap_or(a.permalink)), - genre: value.genre.unwrap_or_default(), - } - } -} - -fn find_str_arg(value: &XTrackItem, arg_type: u32) -> Option<&XArgument> { - value.args.iter().find(|arg| arg.arg_type == arg_type) -} - -impl From for Track { - fn from(value: XTrackItem) -> Self { - Track { - unique_id: value.data.unique_id, - filetype: value.data.filetype, - stars: value.data.stars, - last_modified_time: value.data.last_modified_time, - size: value.data.size, - length: value.data.length, - year: value.data.year, - bitrate: value.data.bitrate, - sample_rate: value.data.sample_rate, - play_count: value.data.play_count, - dbid: value.data.dbid, - bpm: value.data.bpm, - skip_count: value.data.skip_count, - has_artwork: value.data.has_artwork, - media_type: value.data.media_type, - title: find_str_arg(&value, 1).map_or(String::new(), |a| a.val.clone()), - location: find_str_arg(&value, 2).map_or(String::new(), |a| a.val.clone()), - album: find_str_arg(&value, 3).map_or(String::new(), |a| a.val.clone()), - artist: find_str_arg(&value, 4).map_or(String::new(), |a| a.val.clone()), - genre: find_str_arg(&value, 5).map_or(String::new(), |a| a.val.clone()), - } - } -} - -pub fn init_db() -> Database { - Database::create(get_db()).unwrap() -} - -pub fn insert_playlist(db: &Database, playlist: Playlist) -> Result<(), Error> { - let write_txn = db.begin_write()?; - { - let mut table = write_txn.open_table(PLAYLISTS)?; - let uid = playlist.persistent_playlist_id; - let data = bincode::serialize(&playlist).unwrap(); - table.insert(uid, data)?; - } - write_txn.commit()?; - Ok(()) -} - -pub fn get_playlist(db: &Database, id: u64) -> Result { - let read_txn = db.begin_read()?; - let table = read_txn.open_table(PLAYLISTS)?; - let b = table.get(id)?.unwrap().value(); - let value: Playlist = bincode::deserialize(&b).unwrap(); - let playlist = DBPlaylist { - persistent_playlist_id: value.persistent_playlist_id, - timestamp: value.timestamp, - title: value.title, - is_master: value.is_master, - tracks: value.tracks.iter().map(|id| get_track(db, *id)).filter(|t| t.is_ok()).map(|t| t.unwrap()).collect(), - }; - Ok(playlist.into()) -} - -pub fn get_all_playlists(db: &Database) -> Result, Error> { - let read_txn = db.begin_read()?; - let table = read_txn.open_table(PLAYLISTS)?; - Ok(table - .iter() - .unwrap() - .flatten() - .map(|d| bincode::deserialize(&d.1.value()).unwrap()) - .collect::>() - .iter() - .map(|p| DBPlaylist{ - persistent_playlist_id: p.persistent_playlist_id, - timestamp: p.timestamp, - title: p.title.clone(), - is_master: p.is_master, - tracks: p.tracks.iter().map(|id| get_track(db, *id)).filter(|t| t.is_ok()).map(|t| t.unwrap()).collect() - }) - .collect()) -} - -pub fn insert_track(db: &Database, track: Track) -> Result<(), Error> { - let write_txn = db.begin_write()?; - { - let mut table = write_txn.open_table(TRACKS)?; - let uid = track.unique_id; - let data = bincode::serialize(&track).unwrap(); - table.insert(uid, data)?; - - let read_txn = db.begin_read()?; - let table = read_txn.open_table(PLAYLISTS)?; - - let pls = table - .iter() - .unwrap() - .flatten() - .map(|d| bincode::deserialize(&d.1.value()).unwrap()) - .collect::>(); - - for pl in pls { - if !pl.is_master { continue } - let mut master = pl.clone(); - master.tracks.push(uid); - insert_playlist(db, master); - break; - } - } - write_txn.commit()?; - Ok(()) -} - -pub fn get_track(db: &Database, id: u32) -> Result { - let read_txn = db.begin_read()?; - let table = read_txn.open_table(TRACKS)?; - let b = table.get(id)?.unwrap().value(); - let track: Track = bincode::deserialize(&b).unwrap(); - Ok(track) -} - -pub fn get_all_tracks(db: &Database) -> Result, Error> { - let read_txn = db.begin_read()?; - let table = read_txn.open_table(TRACKS)?; - Ok(table - .iter() - .unwrap() - .flatten() - .map(|d| bincode::deserialize(&d.1.value()).unwrap()) - .collect::>()) -} - -pub fn get_last_track_id(db: &Database) -> Result { - let read_txn = db.begin_read()?; - let table = read_txn.open_table(TRACKS)?; - let l = table.last()?.map_or(80u32, |v| v.0.value()); - Ok(l) -} - -// note: this hash function is used to make unique ids for each track. It doesn't aim to generate secure ones. -fn hash(buf: &[u8]) -> u64 { - let mut hasher = Md5::new(); - hasher.update(buf); - let arr = hasher.finalize()[..8].try_into().unwrap(); - u64::from_be_bytes(arr) -} diff --git a/src/main.rs b/src/main.rs index aa4f242..1d0c1af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,6 @@ use tokio_util::sync::CancellationToken; use wait_screen::WaitScreen; mod config; -mod db; mod dlp; mod file_system; mod loading_screen; diff --git a/src/main_screen.rs b/src/main_screen.rs index 06b56a9..e7bac45 100644 --- a/src/main_screen.rs +++ b/src/main_screen.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, TimeZone, Utc}; use crossterm::event::{KeyCode, KeyEvent}; +use itunesdb::xobjects::XPlaylist; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, @@ -10,8 +11,8 @@ use ratatui::{ use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists}; use tokio::sync::mpsc::UnboundedSender; -use crate::{db::Track, screen::AppScreen, sync::AppEvent, theme::Theme}; -use crate::db::DBPlaylist; +use crate::{screen::AppScreen, sync::AppEvent, theme::Theme}; +use crate::sync::DBPlaylist; pub struct MainScreen { mode: bool, @@ -246,11 +247,11 @@ impl MainScreen { ); if let Some(s) = &self.playlists { for (i, playlist) in s.iter().enumerate() { - let date = Utc.timestamp_millis_opt(playlist.timestamp as i64).unwrap(); + let date = Utc.timestamp_millis_opt(playlist.data.timestamp as i64).unwrap(); let mut row = Row::new(vec![ - playlist.persistent_playlist_id.to_string(), + playlist.data.persistent_playlist_id.to_string(), "".to_string(), - playlist.tracks.len().to_string(), + playlist.elems.len().to_string(), format!("{}", date.format("%Y-%m-%d %H:%M")), "YES".to_string(), ]); @@ -328,7 +329,7 @@ impl MainScreen { .style(Style::default().fg(Color::Gray)), ); if let Some(pls) = &self.playlists { - let s = &pls.get(self.selected_playlist as usize).unwrap().tracks; + let s = &pls.get(self.selected_playlist as usize).unwrap().elems; for (i, track) in s.iter().enumerate() { let mut row = Row::new(vec![ track.unique_id.to_string(), diff --git a/src/sync.rs b/src/sync.rs index 8c12d5f..d1dab5e 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,7 +1,6 @@ use std::path::{Path, PathBuf}; -use itunesdb::xobjects::XSomeList; -use redb::Database; +use itunesdb::xobjects::{XDatabase, XTrackItem}; use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists}; use tokio::{ fs::File, @@ -12,8 +11,7 @@ use tokio_util::sync::CancellationToken; use crate::{config::{ get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration, -}, db::{self, Track}, dlp::{self, DownloadProgress}, util, AppState}; -use crate::db::{DBPlaylist, Playlist}; +}, dlp::{self, DownloadProgress}, util, AppState}; pub enum AppEvent { SearchIPod, @@ -26,6 +24,13 @@ pub enum AppEvent { SwitchScreen(AppState), } +pub struct DBPlaylist { + pub id: u64, + pub title: String, + pub timestamp: u32, + pub tracks: Vec +} + pub fn initialize_async_service( sender: Sender, receiver: UnboundedReceiver, @@ -36,7 +41,7 @@ pub fn initialize_async_service( let mut ipod_db = None; - let database = db::init_db(); + let mut database = None; let mut receiver = receiver; @@ -49,13 +54,13 @@ pub fn initialize_async_service( AppEvent::SearchIPod => { if let Some(p) = util::search_ipod() { ipod_db = Some(p.clone()); - parse_itunes(&database, &sender, p).await; + database = Some(parse_itunes(&sender, p).await); let _ = sender.send(AppEvent::SwitchScreen(AppState::MainScreen)).await; } else { let _ = sender.send(AppEvent::IPodNotFound).await; } }, - AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, &database, &sender, ipod_db.as_ref().unwrap().clone()).await, + AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, &mut database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, _ => {} } } @@ -67,7 +72,7 @@ pub fn initialize_async_service( async fn download_playlist( playlist: CloudPlaylist, - database: &Database, + database: &mut XDatabase, sender: &Sender, ipod_path: String ) { @@ -82,15 +87,15 @@ async fn download_playlist( if track.title.is_none() { continue; } - let mut t: Track = track.clone().into(); - t.unique_id = db::get_last_track_id(database).unwrap_or(80) + 1; + let mut t: XTrackItem = track.into(); + t.data.unique_id = database.get_unique_id(); let mut tp = PathBuf::new(); tp.push("iPod_Control"); tp.push("Music"); - tp.push(["F", &format!("{:02}", &(t.unique_id % 100))].concat()); - tp.push(format!("{:X}", t.unique_id)); + tp.push(["F", &format!("{:02}", &(t.data.unique_id % 100))].concat()); + tp.push(format!("{:X}", t.data.unique_id)); tp.set_extension("mp3"); - t.location = tp.to_str().unwrap().to_string().replace(r"/", ":").to_string(); + t.set_location(tp.to_str().unwrap().to_string().replace("/", ":").to_string()); let mut dest = p.clone(); dest.push(tp); @@ -100,7 +105,7 @@ async fn download_playlist( let _ = std::fs::copy(track_path, dest); - let _ = db::insert_track(database, t); + let _ = database.add_track(t); } } let _ = sender @@ -108,7 +113,17 @@ async fn download_playlist( .await; } -async fn parse_itunes(database: &Database, sender: &Sender, path: String) { +fn get_playlists(db: &mut XDatabase) -> Vec { + let pls = db.get_playlists(); // string arg type 1 - playlist title. + pls.iter() + .map(|t| DBPlaylist { + id: t.data.persistent_playlist_id, + title: t.get_title(), + timestamp: t.data.timestamp, + tracks: t.elems.iter().map(|(i, _a)| db.get_track(i.track_id)).filter(|t| t.is_some()).map(|t| t.unwrap().clone()).collect()}).collect() +} + +async fn parse_itunes(sender: &Sender, path: String) -> XDatabase { let cd = get_temp_itunesdb(); let mut p: PathBuf = Path::new(&path).into(); p.push("iPod_Control"); @@ -119,31 +134,11 @@ async fn parse_itunes(database: &Database, sender: &Sender, path: Stri let mut file = File::open(cd).await.unwrap(); let mut contents = vec![]; file.read_to_end(&mut contents).await.unwrap(); - let mut xdb = itunesdb::deserializer::parse_bytes(&contents); - - if let XSomeList::TrackList(tracks) = &xdb.find_dataset(1).child { - for track in tracks { - let t: Track = track.clone().into(); - let _ = db::insert_track(database, t); - } - } - - if let XSomeList::Playlists(playlists) = &xdb.find_dataset(3).child { - for playlist in playlists { - let pl = Playlist { - persistent_playlist_id: playlist.data.persistent_playlist_id, - timestamp: playlist.data.timestamp, - title: String::new() , - is_master: playlist.data.is_master_playlist_flag != 0, - tracks: playlist.elems.iter().map(|e| e.0.track_id).collect() - }; - let _ = db::insert_playlist(database, pl); - } - } + let mut database = itunesdb::deserializer::parse_bytes(&contents); let _ = sender .send(AppEvent::ITunesParsed( - db::get_all_playlists(database).unwrap(), + get_playlists(&mut database), )) .await; @@ -188,4 +183,6 @@ async fn parse_itunes(database: &Database, sender: &Sender, path: Stri collection: playlists, })) .await; + + database }