diff --git a/Cargo.lock b/Cargo.lock index 1ef2c8f..a05ceb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,7 +405,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]] @@ -982,8 +982,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "itunesdb" -version = "0.1.51" -source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#ed639f96d59b6777114551e8c3054f2a0ac9c7da" +version = "0.1.57" +source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#ea14aaf6c3284b2cd83f4628b21f652099a79815" dependencies = [ "bincode", "env_logger", @@ -1536,7 +1536,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1879,7 +1879,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2367,7 +2367,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 061f1a1..4bc8082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ 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.51", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } +itunesdb = { version = "0.1.57", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } puremp3 = "0.1.0" mp3-duration = "0.1.10" rand = "0.8.5" diff --git a/src/dlp.rs b/src/dlp.rs index 1522961..6716c63 100644 --- a/src/dlp.rs +++ b/src/dlp.rs @@ -18,6 +18,55 @@ pub struct DownloadProgress { pub eta: String, } +pub async fn download_track_from_soundcloud( + track_url: &str, + download_dir: &PathBuf, + sender: Sender, +) -> std::result::Result<(), Box> { + let _ = sender + .send(AppEvent::SwitchScreen(crate::AppState::LoadingScreen)) + .await; + + if download_dir.exists() { + let _ = std::fs::remove_dir_all(download_dir); + } + let _ = std::fs::create_dir_all(download_dir); + + let args = &[ + "-f", + "mp3", + "--ignore-errors", + "--newline", + "--progress-template", + "{\"progress_percentage\":\"%(progress._percent_str)s\",\"progress_total\":\"%(progress._total_bytes_str)s\",\"speed\":\"%(progress._speed_str)s\",\"eta\":\"%(progress._eta_str)s\"}", + "-o", + "%(id)i.%(ext)s", + "--write-thumbnail", + track_url + ]; + + let mut command = Command::new("yt-dlp"); + command.args(args); + command.stdout(Stdio::piped()); + command.stderr(Stdio::null()); + command.current_dir(download_dir); + + let mut child = command.spawn()?; + + let stdout = child.stdout.take().unwrap(); + let mut reader = BufReader::new(stdout).lines(); + + while let 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::CurrentProgress(progress)).await; + } + } + let _ = sender.send(AppEvent::OverallProgress((1, 1))).await; + Ok(()) +} + pub async fn download_from_soundcloud( playlist_url: &str, download_dir: &PathBuf, diff --git a/src/file_system.rs b/src/file_system.rs index c6db3ce..79c8d55 100644 --- a/src/file_system.rs +++ b/src/file_system.rs @@ -1,5 +1,6 @@ use crate::component::table::SmartTable; -use crate::{screen::AppScreen, theme::Theme}; +use crate::sync::AppEvent; +use crate::{screen::AppScreen, theme::Theme, AppState}; use chrono::{DateTime, Utc}; use crossterm::event::KeyCode; use ratatui::layout::{Constraint, Direction, Layout, Rect}; @@ -10,30 +11,11 @@ use std::cmp::Ordering; use std::fs::DirEntry; use std::os::unix::fs::MetadataExt; use std::path::PathBuf; +use tokio::sync::mpsc::UnboundedSender; pub struct FileSystem { table: SmartTable, -} - -impl Default for FileSystem { - fn default() -> Self { - let table = SmartTable::new( - ["Name", "Type", "Size", "Modified"] - .iter_mut() - .map(|s| s.to_string()) - .collect(), - vec![ - Constraint::Percentage(50), - Constraint::Length(5), - Constraint::Percentage(20), - Constraint::Percentage(30), - ], - ); - - let mut a = Self { table }; - a.get_path(dirs::document_dir().unwrap()); - a - } + sender: UnboundedSender, } impl AppScreen for FileSystem { @@ -41,6 +23,11 @@ impl AppScreen for FileSystem { match key_event.code { KeyCode::Up => self.table.previous_row(), KeyCode::Down => self.table.next_row(), + KeyCode::F(4) => { + let _ = self + .sender + .send(AppEvent::SwitchScreen(AppState::MainScreen)); + } _ => {} } } @@ -58,6 +45,8 @@ impl AppScreen for FileSystem { // Render Status Bar let status_bar = Paragraph::new(Line::from(vec![ + " SWITCH TO NORMAL".bold(), + " | ".dark_gray(), " SAVE AS PLAYLIST".bold(), " | ".dark_gray(), " SAVE AS IS".bold(), @@ -78,6 +67,25 @@ impl AppScreen for FileSystem { } impl FileSystem { + pub fn new(sender: UnboundedSender) -> Self { + let table = SmartTable::new( + ["Name", "Type", "Size", "Modified"] + .iter_mut() + .map(|s| s.to_string()) + .collect(), + vec![ + Constraint::Percentage(50), + Constraint::Length(5), + Constraint::Percentage(20), + Constraint::Percentage(30), + ], + ); + + let mut a = Self { table, sender }; + a.get_path(dirs::document_dir().unwrap()); + a + } + fn get_path(&mut self, p: PathBuf) { let paths = std::fs::read_dir(&p).unwrap(); let mut dir = paths diff --git a/src/main.rs b/src/main.rs index ff933ff..3127574 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,7 +68,7 @@ impl Default for App { screens.insert(AppState::IPodWait, Box::new(WaitScreen::default())); screens.insert(AppState::MainScreen, Box::new(MainScreen::new(jx.clone()))); screens.insert(AppState::LoadingScreen, Box::new(LoadingScreen::default())); - screens.insert(AppState::FileSystem, Box::new(FileSystem::default())); + screens.insert(AppState::FileSystem, Box::new(FileSystem::new(jx.clone()))); Self { receiver: rx, diff --git a/src/main_screen.rs b/src/main_screen.rs index 699568d..d539c17 100644 --- a/src/main_screen.rs +++ b/src/main_screen.rs @@ -160,14 +160,31 @@ impl MainScreen { fn download_row(&mut self) { if self.selected_tab == 1 { // SC - let playlist = self - .soundcloud - .as_ref() - .unwrap() - .get(self.pl_table.selected_row()) - .unwrap() - .clone(); - let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist)); + match self.mode { + false => { + let playlist = self + .soundcloud + .as_ref() + .unwrap() + .get(self.pl_table.selected_row()) + .unwrap() + .clone(); + let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist)); + } + true => { + let track = self + .soundcloud + .as_ref() + .unwrap() + .get(self.pl_table.selected_row()) + .unwrap() + .tracks + .get(self.song_table.selected_row()) + .unwrap() + .clone(); + let _ = self.sender.send(AppEvent::DownloadTrack(track)); + } + } } } diff --git a/src/sync.rs b/src/sync.rs index f26dfd0..a806c19 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -25,6 +25,7 @@ pub enum AppEvent { ITunesParsed(Vec), SoundcloudGot(CloudPlaylists), DownloadPlaylist(CloudPlaylist), + DownloadTrack(CloudTrack), CurrentProgress(DownloadProgress), OverallProgress((u32, u32)), SwitchScreen(AppState), @@ -114,6 +115,7 @@ pub fn initialize_async_service( } }, AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, + AppEvent::DownloadTrack(track) => download_track(track, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, AppEvent::SwitchScreen(state) => { let _ = sender.send(AppEvent::SwitchScreen(state)).await;}, _ => {} } @@ -124,6 +126,64 @@ pub fn initialize_async_service( }); } +async fn download_track( + track: CloudTrack, + database: &mut XDatabase, + sender: &Sender, + ipod_path: String, +) { + if let Ok(()) = dlp::download_track_from_soundcloud( + &track.permalink_url.clone().unwrap(), + &get_temp_dl_dir(), + sender.clone(), + ) + .await + { + let p: PathBuf = Path::new(&ipod_path).into(); + + let mut t: XTrackItem = track_from_soundcloud(&track); + 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.data.unique_id % 100))].concat()); + tp.push(format!("{:X}", t.data.unique_id)); + tp.set_extension("mp3"); + t.set_location( + tp.to_str() + .unwrap() + .to_string() + .replace("/", ":") + .to_string(), + ); + let mut dest = p.clone(); + dest.push("iPod_Control"); + dest.push("Music"); + dest.push(["F", &format!("{:02}", &(t.data.unique_id % 100))].concat()); + let _ = std::fs::create_dir_all(dest.to_str().unwrap()); + dest.push(format!("{:X}", t.data.unique_id)); + dest.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()); + + database.add_track(t); + } + + let _ = sender + .send(AppEvent::SwitchScreen(AppState::MainScreen)) + .await; + + let _ = sender + .send(AppEvent::ITunesParsed(get_playlists(database))) + .await; + + overwrite_database(database, &ipod_path); +} + async fn download_playlist( playlist: CloudPlaylist, database: &mut XDatabase,