diff --git a/Cargo.lock b/Cargo.lock index 1c46e90..cee09e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1284,8 +1284,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "itunesdb" -version = "0.1.90" -source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#d37957a9a0b66577dadf3b305b02c72f1ca50a62" +version = "0.1.95" +source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#2ecbe7666bf7c561c9ace767d2e4055e0d92127d" dependencies = [ "bincode", "env_logger", @@ -2349,8 +2349,8 @@ dependencies = [ [[package]] name = "soundcloud" -version = "0.1.8" -source = "git+https://gitea.awain.net/alterwain/soundcloud_api.git#656aef28f356411ff25cee14214ea0e677563537" +version = "0.1.9" +source = "git+https://gitea.awain.net/alterwain/soundcloud_api.git#a6732847bebbcb6e59a1b8a44a965fe7092af6e9" dependencies = [ "hyper-util", "regex", diff --git a/Cargo.toml b/Cargo.toml index 4b3310a..00eed0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,8 @@ crossterm = { version = "0.28.1", features = ["event-stream"] } 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.90", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } +soundcloud = { version = "0.1.9", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } +itunesdb = { version = "0.1.95", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } rand = "0.8.5" tui-big-text = "0.7.1" throbber-widgets-tui = "0.8.0" diff --git a/README.md b/README.md index 22adc15..dd4300c 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Lightweight iPod manager, batteries included. -# +#
- +

Screenshot from MacOS Big Sur terminal

@@ -41,6 +41,5 @@ Lightweight iPod manager, batteries included. ## Todos -- Implement artwork integration -- Implement youtube api -- Implement albums generation \ No newline at end of file +- Implement manual playlist editing +- Implement youtube api \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index ff2806f..aee94f8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -32,12 +32,6 @@ pub fn get_temp_itunesdb() -> PathBuf { p } -pub fn get_db() -> PathBuf { - let mut p = get_configs_dir(); - p.push("data.redb"); - p -} - #[derive(Debug, Deserialize, Serialize)] pub struct YouTubeConfiguration { pub user_id: u64, diff --git a/src/dlp.rs b/src/dlp.rs index 1243148..59d12ec 100644 --- a/src/dlp.rs +++ b/src/dlp.rs @@ -1,7 +1,7 @@ -use std::{io, path::PathBuf, process::Stdio}; - +use ratatui::style::Color; use regex::Regex; use serde::Deserialize; +use std::{io, path::PathBuf, process::Stdio}; use tokio::{ io::{AsyncBufReadExt, BufReader}, process::Command, @@ -59,11 +59,15 @@ pub async fn download_track_from_soundcloud( while let Ok(Some(line)) = reader.next_line().await { if line.starts_with("{") { let progress: DownloadProgress = serde_json::from_str(&line).unwrap(); - let _ = sender.send(AppEvent::OverallProgress((0, 1))).await; + let _ = sender + .send(AppEvent::OverallProgress((0, 1, Color::Green))) + .await; let _ = sender.send(AppEvent::CurrentProgress(progress)).await; } } - let _ = sender.send(AppEvent::OverallProgress((1, 1))).await; + let _ = sender + .send(AppEvent::OverallProgress((1, 1, Color::Green))) + .await; Ok(()) } @@ -114,7 +118,9 @@ pub async fn download_from_soundcloud( let s: Vec<&str> = s.split(' ').collect(); let cur = s.first().unwrap().trim().parse().unwrap(); let max = s.last().unwrap().trim().parse().unwrap(); - let _ = sender.send(AppEvent::OverallProgress((cur, max))).await; + let _ = sender + .send(AppEvent::OverallProgress((cur, max, Color::Green))) + .await; } None => { if line.starts_with("{") { diff --git a/src/file_system.rs b/src/file_system.rs index b7f889e..9b23771 100644 --- a/src/file_system.rs +++ b/src/file_system.rs @@ -21,13 +21,30 @@ pub struct FileSystem { sender: UnboundedSender, } -fn check_extension_compatibility(ext: &OsStr) -> bool { +fn check_extension_compatibility(ext: &str) -> bool { matches!( - ext.to_str().unwrap().to_lowercase().as_str(), + ext.to_lowercase().as_str(), "mp3" | "m4a" | "wav" | "aiff" | "aif" ) } +fn get_extension_from_filename(file_name: Option<&OsStr>) -> String { + if let Some(fname) = file_name { + let file_name = fname.to_str().unwrap(); + let index = file_name + .chars() + .enumerate() + .filter(|(i, c)| *c == '.') + .map(|(i, c)| i) + .last(); + if let Some(index) = index { + let extension: String = file_name.chars().skip(index).collect(); + return extension; + } + } + "none".to_string() +} + fn list_files_recursively(p: PathBuf) -> Vec { let mut files = Vec::new(); @@ -38,7 +55,14 @@ fn list_files_recursively(p: PathBuf) -> Vec { continue; } let a = path.unwrap().path(); - if a.is_file() && check_extension_compatibility(a.extension().unwrap()) { + if a.is_file() + && check_extension_compatibility( + a.extension() + .map_or(&get_extension_from_filename(a.file_name()), |s| { + s.to_str().unwrap() + }), + ) + { files.push(a.clone()); } if a.is_dir() { @@ -130,10 +154,9 @@ impl FileSystem { let mut dir = paths .filter_map(|res| res.ok()) .filter(|p| { - p.path() - .extension() - .map_or(false, check_extension_compatibility) - || p.path().is_dir() + p.path().extension().map_or(false, |s| { + check_extension_compatibility(s.to_str().unwrap_or("none")) + }) || p.path().is_dir() }) .collect::>(); dir.sort_by(|a, b| { diff --git a/src/loading_screen.rs b/src/loading_screen.rs index 249f8a4..591a60b 100644 --- a/src/loading_screen.rs +++ b/src/loading_screen.rs @@ -10,7 +10,8 @@ use crate::{dlp::DownloadProgress, screen::AppScreen, theme::Theme}; #[derive(Default)] pub struct LoadingScreen { - pub progress: Option<(u32, u32)>, + pub progress: Option<(u32, u32, ratatui::style::Color)>, + pub artwork_progress: Option<(u32, u32)>, pub s_progress: Option, } @@ -61,11 +62,29 @@ impl LoadingScreen { self.render_overall(frame, chunks[1]); } - if self.s_progress.is_some() { + if self.artwork_progress.is_some() { + self.render_artwork_progress(frame, chunks[2]); + } else if self.s_progress.is_some() { self.render_current(frame, chunks[2]); } } + fn render_artwork_progress(&self, frame: &mut Frame, area: Rect) { + let gauge = Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .title(" Generating album covers "), + ) + .gauge_style(Style::default().fg(Color::LightBlue)) + .ratio( + self.artwork_progress.unwrap().0 as f64 / self.artwork_progress.unwrap().1 as f64, + ) + .label("Generating album covers..."); + + frame.render_widget(gauge, area); + } + fn render_current(&self, frame: &mut Frame, area: Rect) { let s: String = self .s_progress @@ -96,7 +115,7 @@ impl LoadingScreen { .borders(Borders::ALL) .title(" Downloading Playlist "), ) - .gauge_style(Style::default().fg(Color::Green)) + .gauge_style(Style::default().fg(self.progress.unwrap().2)) .ratio(self.progress.unwrap().0 as f64 / self.progress.unwrap().1 as f64) .label(format!( "{:}/{:}", diff --git a/src/main.rs b/src/main.rs index 3127574..c912930 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,14 +121,22 @@ impl App { let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen); screen.set_soundcloud_playlists(playlists); }, - AppEvent::OverallProgress((c, max)) => { + AppEvent::OverallProgress((c, max, color)) => { + self.state = AppState::LoadingScreen; let screen: &mut LoadingScreen = self.get_screen(&AppState::LoadingScreen); - screen.progress = Some((c, max)); + screen.progress = Some((c, max, color)); + screen.artwork_progress = None; }, AppEvent::CurrentProgress(progress) => { let screen: &mut LoadingScreen = self.get_screen(&AppState::LoadingScreen); + screen.artwork_progress = None; screen.s_progress = Some(progress); }, + AppEvent::ArtworkProgress((c, max)) => { + let screen: &mut LoadingScreen = self.get_screen(&AppState::LoadingScreen); + screen.artwork_progress = Some((c, max)); + screen.s_progress = None; + }, AppEvent::SwitchScreen(screen) => { self.state = screen; } diff --git a/src/sync.rs b/src/sync.rs index 52bdf12..ac02f1e 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -6,6 +6,7 @@ use itunesdb::artworkdb::aobjects::ADatabase; use itunesdb::objects::{ListSortOrder, PlaylistItem}; use itunesdb::serializer; use itunesdb::xobjects::{XDatabase, XPlArgument, XPlaylist, XTrackItem}; +use ratatui::style::Color; use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists, CloudTrack}; use std::io::Read; use std::io::{Cursor, Write}; @@ -34,7 +35,8 @@ pub enum AppEvent { DownloadPlaylist(CloudPlaylist), DownloadTrack(CloudTrack), CurrentProgress(DownloadProgress), - OverallProgress((u32, u32)), + OverallProgress((u32, u32, ratatui::style::Color)), + ArtworkProgress((u32, u32)), SwitchScreen(AppState), LoadFromFS(PathBuf), LoadFromFSVec(Vec), @@ -51,7 +53,11 @@ pub struct DBPlaylist { pub tracks: Vec, } -async fn track_from_soundcloud(value: &CloudTrack, ipod_path: String) -> Option { +async fn track_from_soundcloud( + value: &CloudTrack, + ipod_path: String, + sender: &Sender, +) -> Option { let mut track_path = get_temp_dl_dir(); track_path.push(value.id.to_string()); track_path.set_extension("mp3"); @@ -62,7 +68,6 @@ async fn track_from_soundcloud(value: &CloudTrack, ipod_path: String) -> Option< .await .unwrap(); let audio_info = &audio_file.audio_file.tracks.track; - let song_dbid = util::hash_from_path(track_path); let mut track = XTrackItem::new( @@ -77,20 +82,24 @@ async fn track_from_soundcloud(value: &CloudTrack, ipod_path: String) -> Option< ); if image_path.exists() { + let _ = sender.send(AppEvent::ArtworkProgress((0, 2))).await; let mut adb = get_artwork_db(&ipod_path); let image_data = std::fs::read(image_path).unwrap(); - let (small_img_name, large_img_name) = adb.add_images(song_dbid, util::hash(&image_data)); + let cover_hash = util::hash(&image_data); - let mut dst = PathBuf::from(&ipod_path); - dst.push("iPod_Control"); - dst.push("Artwork"); + let if_cover_present = adb.if_cover_present(cover_hash); + + let (small_img_name, large_img_name) = adb.add_images(song_dbid, cover_hash); let size = image_data.len(); - make_cover_image(&image_data, &ipod_path, &small_img_name, (100, 100)); - make_cover_image(&image_data, &ipod_path, &large_img_name, (200, 200)); + if !if_cover_present { + make_cover_image(&image_data, &ipod_path, &small_img_name, (100, 100)); + let _ = sender.send(AppEvent::ArtworkProgress((1, 2))).await; + make_cover_image(&image_data, &ipod_path, &large_img_name, (200, 200)); + } write_artwork_db(adb, &ipod_path); @@ -98,6 +107,7 @@ async fn track_from_soundcloud(value: &CloudTrack, ipod_path: String) -> Option< track.data.mhii_link = 0; track.data.has_artwork = 1; track.data.artwork_count = 1; + let _ = sender.send(AppEvent::ArtworkProgress((2, 2))).await; } audio_file.modify_xtrack(&mut track); @@ -109,7 +119,9 @@ async fn track_from_soundcloud(value: &CloudTrack, ipod_path: String) -> Option< .clone() .map_or(String::new(), |a| a.username.unwrap_or(a.permalink)), ); - track.set_genre(value.genre.clone().unwrap()); + if value.genre.is_some() { + track.set_genre(value.genre.clone().unwrap()); + } Some(track) } @@ -207,11 +219,15 @@ async fn remove_track_from_playlist( sender: &Sender, ipod_path: String, ) { - let _ = sender.send(AppEvent::OverallProgress((0, 1))).await; + let _ = sender + .send(AppEvent::OverallProgress((0, 1, Color::Red))) + .await; database.remove_track_from_playlist(track_id, pl_id); - let _ = sender.send(AppEvent::OverallProgress((1, 1))).await; + let _ = sender + .send(AppEvent::OverallProgress((1, 1, Color::Red))) + .await; let _ = sender .send(AppEvent::SwitchScreen(AppState::MainScreen)) @@ -242,18 +258,22 @@ async fn remove_playlist( let mut i = 1; for (item, args) in pl.elems.iter() { let _ = sender - .send(AppEvent::OverallProgress((i, max as u32))) + .send(AppEvent::OverallProgress((i, max as u32, Color::Red))) .await; remove_track(item.track_id, database, sender, ipod_path.clone()).await; i += 1; } } - let _ = sender.send(AppEvent::OverallProgress((0, 1))).await; + let _ = sender + .send(AppEvent::OverallProgress((0, 1, Color::Red))) + .await; database.remove_playlist(pl_id); - let _ = sender.send(AppEvent::OverallProgress((1, 1))).await; + let _ = sender + .send(AppEvent::OverallProgress((1, 1, Color::Red))) + .await; let _ = sender .send(AppEvent::SwitchScreen(AppState::MainScreen)) @@ -272,14 +292,18 @@ async fn remove_track( sender: &Sender, ipod_path: String, ) { - let _ = sender.send(AppEvent::OverallProgress((0, 1))).await; + let _ = sender + .send(AppEvent::OverallProgress((0, 1, Color::Red))) + .await; database.remove_track_completely(id); for ext in ["mp3", "m4a", "wav", "aif"].iter() { let dest = get_full_track_location(PathBuf::from(ipod_path.clone()), id, ext); let _ = std::fs::remove_file(dest); } - let _ = sender.send(AppEvent::OverallProgress((1, 1))).await; + let _ = sender + .send(AppEvent::OverallProgress((1, 1, Color::Red))) + .await; let _ = sender .send(AppEvent::SwitchScreen(AppState::MainScreen)) @@ -305,7 +329,11 @@ async fn load_files_from_fs_as_playlist( for (i, file) in files.iter().enumerate() { let _ = sender - .send(AppEvent::OverallProgress((i as u32, files.len() as u32))) + .send(AppEvent::OverallProgress(( + i as u32, + files.len() as u32, + Color::Green, + ))) .await; let id = load_from_fs(file.clone(), database, sender, ipod_path.clone()).await; @@ -333,7 +361,11 @@ async fn load_files_from_fs( ) { for (i, file) in files.iter().enumerate() { let _ = sender - .send(AppEvent::OverallProgress((i as u32, files.len() as u32))) + .send(AppEvent::OverallProgress(( + i as u32, + files.len() as u32, + Color::Green, + ))) .await; load_from_fs(file.clone(), database, sender, ipod_path.clone()).await; } @@ -347,7 +379,7 @@ async fn load_from_fs( ) -> u32 { let tag = Tag::new().read_from_path(&path).unwrap(); - let id = database.get_unique_id(); + let mut id = database.get_unique_id(); let audio_file = audio_file_info::from_path(path.to_str().unwrap()) .await @@ -356,75 +388,86 @@ async fn load_from_fs( let song_dbid = util::hash_from_path(path.clone()); - let mut track = XTrackItem::new( - id, - audio_info.audio_bytes as u32, - (audio_info.duration * 1000.0) as u32, - tag.year().unwrap_or(0) as u32, - (audio_info.bit_rate / 1000) as u32, - audio_info.sample_rate as u32, - song_dbid, - 0, - ); + if !database.if_track_in_library(song_dbid) { + let mut track = XTrackItem::new( + id, + audio_info.audio_bytes as u32, + (audio_info.duration * 1000.0) as u32, + tag.year().unwrap_or(0) as u32, + (audio_info.bit_rate / 1000) as u32, + audio_info.sample_rate as u32, + song_dbid, + 0, + ); - audio_file.modify_xtrack(&mut track); + audio_file.modify_xtrack(&mut track); - if let Some(title) = tag.title() { - track.set_title(title.to_string()); - } else { - track.set_title(path.file_name().unwrap().to_str().unwrap().to_string()); + if let Some(title) = tag.title() { + track.set_title(title.to_string()); + } else { + track.set_title(path.file_name().unwrap().to_str().unwrap().to_string()); + } + + if let Some(genre) = tag.genre() { + track.set_genre(genre.to_string()); + } + + if let Some(artist) = tag.artist() { + track.set_artist(artist.to_string()); + } + + if let Some(cover) = tag.album_cover() { + let _ = sender.send(AppEvent::ArtworkProgress((0, 2))).await; + + let mut adb = get_artwork_db(&ipod_path); + + let cover_hash = util::hash(cover.data); + + let if_cover_present = adb.if_cover_present(cover_hash); + + let (small_img_name, large_img_name) = adb.add_images(song_dbid, cover_hash); + + let size = cover.data.len(); + + if !if_cover_present { + make_cover_image(cover.data, &ipod_path, &small_img_name, (100, 100)); + let _ = sender.send(AppEvent::ArtworkProgress((1, 2))).await; + make_cover_image(cover.data, &ipod_path, &large_img_name, (200, 200)); + } + + write_artwork_db(adb, &ipod_path); + + track.data.artwork_size = size as u32; + track.data.mhii_link = 0; + track.data.has_artwork = 1; + track.data.artwork_count = 1; + + let _ = sender.send(AppEvent::ArtworkProgress((2, 2))).await; + } + + if let Some(album) = tag.album() { + track.set_album(album.title.to_string()); + // TODO: Add new album into iTunesDB + } + + track.set_location(get_track_location( + track.data.unique_id, + audio_file.get_audio_extension(), + )); + + let dest = get_full_track_location( + PathBuf::from(ipod_path.clone()), + track.data.unique_id, + audio_file.get_audio_extension(), + ); + + let _ = std::fs::copy(path.to_str().unwrap(), dest.to_str().unwrap()); + + database.add_track(track); + } else if let Some(unique_id) = database.get_unique_id_by_dbid(song_dbid) { + id = unique_id; } - if let Some(genre) = tag.genre() { - track.set_genre(genre.to_string()); - } - - if let Some(artist) = tag.artist() { - track.set_artist(artist.to_string()); - } - - if let Some(cover) = tag.album_cover() { - let mut adb = get_artwork_db(&ipod_path); - - let (small_img_name, large_img_name) = adb.add_images(song_dbid, util::hash(cover.data)); - - let mut dst = PathBuf::from(&ipod_path); - dst.push("iPod_Control"); - dst.push("Artwork"); - - let size = cover.data.len(); - - make_cover_image(cover.data, &ipod_path, &small_img_name, (100, 100)); - make_cover_image(cover.data, &ipod_path, &large_img_name, (200, 200)); - - write_artwork_db(adb, &ipod_path); - - track.data.artwork_size = size as u32; - track.data.mhii_link = 0; - track.data.has_artwork = 1; - track.data.artwork_count = 1; - } - - if let Some(album) = tag.album() { - track.set_album(album.title.to_string()); - // TODO: Add new album into iTunesDB - } - - track.set_location(get_track_location( - track.data.unique_id, - audio_file.get_audio_extension(), - )); - - let dest = get_full_track_location( - PathBuf::from(ipod_path.clone()), - track.data.unique_id, - audio_file.get_audio_extension(), - ); - - let _ = std::fs::copy(path.to_str().unwrap(), dest.to_str().unwrap()); - - database.add_track(track); - let _ = sender .send(AppEvent::SwitchScreen(AppState::MainScreen)) .await; @@ -522,18 +565,20 @@ async fn download_track( { let p: PathBuf = Path::new(&ipod_path).into(); - if let Some(mut t) = track_from_soundcloud(&track, ipod_path.clone()).await { - t.data.unique_id = database.get_unique_id(); - t.set_location(get_track_location(t.data.unique_id, "mp3")); - let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3"); + if let Some(mut t) = track_from_soundcloud(&track, ipod_path.clone(), sender).await { + if !database.if_track_in_library(t.data.dbid) { + t.data.unique_id = database.get_unique_id(); + t.set_location(get_track_location(t.data.unique_id, "mp3")); + let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3"); - let mut track_path = get_temp_dl_dir(); - track_path.push(track.id.to_string()); - track_path.set_extension("mp3"); + let mut track_path = get_temp_dl_dir(); + track_path.push(track.id.to_string()); + track_path.set_extension("mp3"); - let _ = std::fs::copy(track_path.to_str().unwrap(), dest.to_str().unwrap()); + let _ = std::fs::copy(track_path.to_str().unwrap(), dest.to_str().unwrap()); - database.add_track(t); + database.add_track(t); + } } } @@ -572,18 +617,21 @@ async fn download_playlist( if track.title.is_none() { continue; } - if let Some(mut t) = track_from_soundcloud(&track, ipod_path.clone()).await { - t.data.unique_id = database.get_unique_id(); - new_playlist.add_elem(t.data.unique_id); - t.set_location(get_track_location(t.data.unique_id, "mp3")); - let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3"); - let mut track_path = get_temp_dl_dir(); - track_path.push(track.id.to_string()); - track_path.set_extension("mp3"); + if let Some(mut t) = track_from_soundcloud(&track, ipod_path.clone(), sender).await { + if !database.if_track_in_library(t.data.dbid) { + t.data.unique_id = database.get_unique_id(); + new_playlist.add_elem(t.data.unique_id); + t.set_location(get_track_location(t.data.unique_id, "mp3")); + let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3"); + let mut track_path = get_temp_dl_dir(); + track_path.push(track.id.to_string()); + track_path.set_extension("mp3"); - let _ = std::fs::copy(track_path.to_str().unwrap(), dest.to_str().unwrap()); - - database.add_track(t); + let _ = std::fs::copy(track_path.to_str().unwrap(), dest.to_str().unwrap()); + database.add_track(t); + } else if let Some(unique_id) = database.get_unique_id_by_dbid(t.data.dbid) { + new_playlist.add_elem(unique_id); + } } } @@ -663,14 +711,17 @@ async fn parse_itunes(sender: &Sender, path: String) -> XDatabase { let mut playlists = playlists.collection; for playlist in playlists.iter_mut() { - if let Ok(tracks) = soundcloud::get_tracks( - playlist.tracks.clone(), - client_id.clone(), - app_version.clone(), - ) - .await - { - playlist.tracks = tracks; + let trr = playlist.tracks.clone(); + playlist.tracks = Vec::new(); + for pl_tracks in trr.clone().chunks(45) { + if let Ok(tracks) = + soundcloud::get_tracks(pl_tracks.to_vec(), client_id.clone(), app_version.clone()) + .await + { + let mut tracks = tracks; + tracks.retain(|t| t.title.is_some()); + playlist.tracks.append(&mut tracks); + } } }