diff --git a/Cargo.lock b/Cargo.lock index e8e517f..a72805d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,7 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1021,8 +1021,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "itunesdb" -version = "0.1.2" -source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#2db99df934c29f03b842a43245f1e93d7d4ade27" +version = "0.1.6" +source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#eac020520b7efa2173065772105ab0b2c4ba6da6" dependencies = [ "bincode", "env_logger", @@ -1662,7 +1662,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2029,7 +2029,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2532,7 +2532,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2b6fa8f..07a1ef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7.12", features = ["codec"] } strum = { version = "0.27", features = ["derive"] } soundcloud = { version = "0.1.8", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } -itunesdb = { version = "0.1.2", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } +itunesdb = { version = "0.1.6", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } ureq = "3.0.5" color-thief = "0.2" redb = "2.4.0" diff --git a/src/db.rs b/src/db.rs index cc1f914..0baed04 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,6 +1,6 @@ use std::fs::File; -use itunesdb::xobjects::{XArgument, XTrackItem}; +use itunesdb::xobjects::{XArgument, XPlaylist, XTrackItem}; use md5::{Digest, Md5}; use redb::{Database, Error, ReadableTable, TableDefinition}; use serde::{Deserialize, Serialize}; @@ -9,6 +9,7 @@ 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 { @@ -17,7 +18,7 @@ pub struct Track { stars: u8, last_modified_time: u32, size: u32, - length: u32, + pub length: u32, year: u32, pub bitrate: u32, sample_rate: u32, @@ -31,7 +32,25 @@ pub struct Track { location: String, album: String, pub artist: String, - genre: 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)] +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 { @@ -100,12 +119,57 @@ impl From for Track { } } -// TODO: implement From (or Into) for Track, convert from Soundcloud Audio or iTunes - 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()?; { diff --git a/src/main.rs b/src/main.rs index 053f104..aa4f242 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,9 +111,9 @@ impl App { AppEvent::IPodNotFound => { let _ = self.sender.send(AppEvent::SearchIPod); }, - AppEvent::ITunesParsed(tracks) => { + AppEvent::ITunesParsed(playlists) => { let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen); - screen.tracks = Some(tracks); + screen.set_itunes(playlists); }, AppEvent::SoundcloudGot(playlists) => { let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen); diff --git a/src/main_screen.rs b/src/main_screen.rs index 2853617..3050ab0 100644 --- a/src/main_screen.rs +++ b/src/main_screen.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, TimeZone, Utc}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, @@ -11,6 +11,7 @@ use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists}; use tokio::sync::mpsc::UnboundedSender; use crate::{db::Track, screen::AppScreen, sync::AppEvent, theme::Theme}; +use crate::db::DBPlaylist; pub struct MainScreen { mode: bool, @@ -21,7 +22,7 @@ pub struct MainScreen { max_songs: i32, tab_titles: Vec, soundcloud: Option>, - pub tracks: Option>, + playlists: Option>, sender: UnboundedSender, } @@ -97,12 +98,12 @@ impl MainScreen { max_pls: 0, max_songs: 0, soundcloud: None, - tracks: None, + playlists: None, selected_tab: 0, tab_titles: vec![ "YouTube".to_string(), "SoundCloud".to_string(), - "Local Playlists".to_string(), + "iPod".to_string(), "Settings".to_string(), ], sender, @@ -115,7 +116,7 @@ impl MainScreen { self.max_songs = 0; self.max_pls = match self.selected_tab { 1 => self.soundcloud.as_deref().unwrap_or(&[]).len(), - 2 => self.tracks.as_deref().unwrap_or(&[]).len(), + 2 => self.playlists.as_deref().unwrap_or(&[]).len(), _ => 0, } .try_into() @@ -201,6 +202,13 @@ impl MainScreen { self.update_max_rows(); } } + + pub fn set_itunes(&mut self, pl: Vec) { + self.playlists = Some(pl); + if self.selected_tab == 2 { + self.update_max_rows(); + } + } fn render_tab(&self, frame: &mut Frame, area: Rect) { let rows = match self.selected_tab { @@ -233,17 +241,18 @@ impl MainScreen { // local let mut v = Vec::new(); v.push( - Row::new(vec!["Id", "Title", "Artist", "Bitrate", "Hash"]) + Row::new(vec!["Id", "Title", "Songs Count", "Date", "IS"]) .style(Style::default().fg(Color::Gray)), ); - if let Some(s) = &self.tracks { - for (i, track) in s.iter().enumerate() { + 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 mut row = Row::new(vec![ - track.unique_id.to_string(), - track.title.clone(), - track.artist.clone(), - track.bitrate.to_string(), - format!("{:X}", track.dbid), + playlist.persistent_playlist_id.to_string(), + "".to_string(), + playlist.tracks.len().to_string(), + format!("{}", date.format("%Y-%m-%d %H:%M")), + "YES".to_string(), ]); if self.selected_playlist == i as i32 { row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White)); @@ -282,7 +291,7 @@ impl MainScreen { let rows = match self.selected_tab { 1 => { - // local + // sc let mut v = Vec::new(); v.push( Row::new(vec!["Id", "Title", "Artist", "Duration", "Genre"]) @@ -310,6 +319,31 @@ impl MainScreen { } } v + }, + 2 => { + // local + let mut v = Vec::new(); + v.push( + Row::new(vec!["Id", "Title", "Artist", "Bitrate", "Genre"]) + .style(Style::default().fg(Color::Gray)), + ); + if let Some(pls) = &self.playlists { + let s = &pls.get(self.selected_playlist as usize).unwrap().tracks; + for (i, track) in s.iter().enumerate() { + let mut row = Row::new(vec![ + track.unique_id.to_string(), + track.title.clone(), + track.artist.clone(), + track.bitrate.to_string(), + track.genre.clone(), + ]); + if self.selected_song == i as i32 { + row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White)); + } + v.push(row); + } + } + v } _ => Vec::new(), }; diff --git a/src/sync.rs b/src/sync.rs index acf6ea2..2146ee4 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -10,19 +10,15 @@ use tokio::{ }; 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}, - AppState, -}; +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}; pub enum AppEvent { SearchIPod, IPodNotFound, - ITunesParsed(Vec), + ITunesParsed(Vec), SoundcloudGot(CloudPlaylists), DownloadPlaylist(CloudPlaylist), CurrentProgress(DownloadProgress), @@ -38,6 +34,8 @@ pub fn initialize_async_service( tokio::spawn(async move { let _ = std::fs::create_dir_all(get_configs_dir()); + let mut ipod_db = None; + let database = db::init_db(); let mut receiver = receiver; @@ -49,13 +47,13 @@ pub fn initialize_async_service( if let Some(request) = r { match request { AppEvent::SearchIPod => { - /*if let Some(p) = util::search_ipod() { - let _ = sender.send(AppEvent::IPodFound(p)).await; + if let Some(p) = util::search_ipod() { + let _ = sender.send(AppEvent::SwitchScreen(AppState::MainScreen)).await; + ipod_db = Some(p.clone()); + parse_itunes(&database, &sender, p).await; } else { let _ = sender.send(AppEvent::IPodNotFound).await; - }*/ - let _ = sender.send(AppEvent::SwitchScreen(AppState::MainScreen)).await; - parse_itunes(&database, &sender, "/Users/michael/Documents/ipod/iTunes/iTunesDB".to_string()).await; + } }, AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, &database, &sender).await, _ => {} @@ -92,12 +90,11 @@ async fn download_playlist( } async fn parse_itunes(database: &Database, sender: &Sender, path: String) { - // todo: parse itunes let cd = get_temp_itunesdb(); - let p: PathBuf = Path::new(&path).into(); - // p.push("iPod_Control"); - // p.push("iTunes"); - // p.set_file_name("iTunesDB"); + let mut p: PathBuf = Path::new(&path).into(); + p.push("iPod_Control"); + p.push("iTunes"); + p.set_file_name("iTunesDB"); let _ = std::fs::copy(p, &cd); let mut file = File::open(cd).await.unwrap(); let mut contents = vec![]; @@ -111,9 +108,22 @@ async fn parse_itunes(database: &Database, sender: &Sender, path: Stri } } + 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 _ = sender .send(AppEvent::ITunesParsed( - db::get_all_tracks(database).unwrap(), + db::get_all_playlists(database).unwrap(), )) .await;