diff --git a/Cargo.lock b/Cargo.lock index a4a6a07..acd3407 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -984,6 +984,7 @@ dependencies = [ "regex", "rusb", "serde", + "serde_json", "soundcloud", "strum 0.27.0", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 1a14b07..0acf425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ rusb = "0.9.4" dirs = "6.0.0" toml = "0.8.20" serde = "1.0.217" +serde_json = "1.0" regex = "1.11.1" ratatui = { version = "0.29.0", features = ["all-widgets"] } color-eyre = "0.6.3" diff --git a/src/dlp.rs b/src/dlp.rs index 6d9fecf..20070b0 100644 --- a/src/dlp.rs +++ b/src/dlp.rs @@ -1,13 +1,32 @@ -use std::path::PathBuf; +use std::{path::PathBuf, process::Stdio}; -use tokio::process::Command; +use regex::Regex; +use serde::Deserialize; +use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::Sender}; + +use crate::sync::AppEvent; + +#[derive(Debug, Deserialize)] +pub struct DownloadProgress { + progress_percentage: String, + progress_total: String, + speed: String, + eta: String +} + +pub async fn download_from_soundcloud(playlist_url: &str, download_dir: &PathBuf, sender: Sender) -> std::result::Result<(), Box> { + let dl_rx: Regex = Regex::new(r"\[download\] Downloading item \d+ of \d+").unwrap(); + + if download_dir.exists() { + let _ = std::fs::remove_dir_all(download_dir); + } + let _ = std::fs::create_dir_all(download_dir); -pub async fn download_from_soundcloud(playlist_url: &str, download_dir: &PathBuf) -> std::result::Result<(), Box> { let args = &[ "--ignore-errors", "--newline", "--progress-template", - "{\"progressPercentage\":\"%(progress._percent_str)s\",\"progressTotal\":\"%(progress._total_bytes_str)s\",\"speed\":\"%(progress._speed_str)s\",\"ETA\":\"%(progress._eta_str)s\"}", + "{\"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", @@ -16,13 +35,34 @@ pub async fn download_from_soundcloud(playlist_url: &str, download_dir: &PathBuf 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 mut stdout = Vec::new(); - let child_stdout = child.stdout.take(); - tokio::io::copy(&mut child_stdout.unwrap(), &mut stdout).await.unwrap(); + + let stdout = child.stdout.take().unwrap(); + let mut reader = BufReader::new(stdout).lines(); + + while let Some(line) = reader.next_line().await? { + match dl_rx.find(&line) { + Some(m) => { + let mut s = m.as_str(); + s = s.split("Downloading item ").last().unwrap(); + 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; + }, + None => { + if line.starts_with("{") { + let progress: DownloadProgress = serde_json::from_str(&line).unwrap(); + let _ = sender.send(AppEvent::CurrentProgress(progress)).await; + } + } + } + } Ok(()) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fee6ad5..6f5432d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ pub struct App { impl Default for App { fn default() -> Self { - let (tx, mut rx) = mpsc::channel(1); + let (tx, mut rx) = mpsc::channel(10); let (jx, mut jr) = mpsc::unbounded_channel(); let token = CancellationToken::new(); @@ -45,7 +45,7 @@ impl Default for App { let mut screens: HashMap> = HashMap::new(); screens.insert(AppState::IPodWait, Box::new(WaitScreen::default())); - screens.insert(AppState::MainScreen, Box::new(MainScreen::new())); + screens.insert(AppState::MainScreen, Box::new(MainScreen::new(jx.clone()))); Self { receiver: rx, sender: jx, token, state: AppState::IPodWait, screens } } @@ -65,12 +65,6 @@ impl App { } fn handle_events(&mut self) -> io::Result<()> { - match event::read()? { - Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { - self.handle_key_event(key_event) - } - _ => {} - }; if let Ok(event) = self.receiver.try_recv() { match event { AppEvent::IPodFound(path) => { @@ -85,15 +79,24 @@ impl App { }, AppEvent::SoundcloudGot(playlists) => { let a = self.screens.get_mut(&AppState::MainScreen).unwrap(); - let screen: &mut MainScreen = match a.as_any().downcast_mut::() { - Some(b) => b, - None => panic!("&a isn't a B!"), - }; + let screen: &mut MainScreen = a.as_any().downcast_mut::().unwrap(); screen.soundcloud = Some(playlists); + }, + AppEvent::OverallProgress((c, max)) => { + let a = self.screens.get_mut(&AppState::MainScreen).unwrap(); + let screen: &mut MainScreen = a.as_any().downcast_mut::().unwrap(); + screen.progress = Some((c, max)); + screen.download_screen(); } _ => {} } - } + }; + match event::read()? { + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + self.handle_key_event(key_event) + } + _ => {} + }; Ok(()) } diff --git a/src/main_screen.rs b/src/main_screen.rs index 5e069a6..b8426af 100644 --- a/src/main_screen.rs +++ b/src/main_screen.rs @@ -1,11 +1,12 @@ use chrono::{DateTime, Utc}; use color_eyre::owo_colors::OwoColorize; use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, Paragraph, Row, Table, Tabs, Widget}, Frame}; +use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Row, Table, Tabs, Widget}, Frame}; use soundcloud::sobjects::CloudPlaylists; use strum::IntoEnumIterator; +use tokio::sync::mpsc::UnboundedSender; -use crate::{config::get_temp_dl_dir, dlp, screen::AppScreen}; +use crate::{config::get_temp_dl_dir, dlp, screen::AppScreen, sync::AppEvent}; #[derive(Debug, Clone)] pub struct MainScreen { @@ -13,7 +14,9 @@ pub struct MainScreen { selected_row: i32, max_rows: i32, tab_titles: Vec, - pub soundcloud: Option + pub soundcloud: Option, + pub progress: Option<(u32, u32)>, + sender: UnboundedSender } impl AppScreen for MainScreen { @@ -48,9 +51,11 @@ impl AppScreen for MainScreen { frame.render_widget(tabs, chunks[0]); - /*let main_content = Paragraph::new("Main content goes here!") - .block(Block::default().borders(Borders::ALL).title("Main")); */ - frame.render_widget(self.render_tab(), chunks[1]); // Render into second chunk + if self.selected_tab != -1 { + frame.render_widget(self.render_tab(), chunks[1]); // Render into second chunk + } else { + self.render_progress(frame, chunks[1]); + } // Render Status Bar let status_bar = Paragraph::new( @@ -68,16 +73,37 @@ impl AppScreen for MainScreen { } impl MainScreen { - pub fn new() -> Self { - MainScreen { selected_row: -1, max_rows: 0, soundcloud: None, selected_tab: 0, tab_titles: vec!["YouTube".to_string(), "SoundCloud".to_string(), "Local Playlists".to_string(), "Settings".to_string()] } + pub fn new( sender: UnboundedSender ) -> Self { + MainScreen { + selected_row: -1, + max_rows: 0, + soundcloud: None, + progress: None, + selected_tab: 0, + tab_titles: vec!["YouTube".to_string(), "SoundCloud".to_string(), "Local Playlists".to_string(), "Settings".to_string()], + sender + } + } + + pub fn download_screen(&mut self) { + self.selected_tab = -1; + } + + fn update_max_rows(&mut self) { + self.max_rows = match self.selected_tab { + 1 => self.soundcloud.as_ref().unwrap_or( &CloudPlaylists { collection: Vec::new() }).collection.len(), + _ => 0 + }.try_into().unwrap(); } fn next_tab(&mut self) { - self.selected_tab = std::cmp::min(self.selected_tab+1, (self.tab_titles.len()-1).try_into().unwrap()) + self.selected_tab = std::cmp::min(self.selected_tab+1, (self.tab_titles.len()-1).try_into().unwrap()); + self.update_max_rows(); } fn previous_tab(&mut self) { self.selected_tab = std::cmp::max(0, self.selected_tab-1); + self.update_max_rows(); } fn previous_row(&mut self) { @@ -85,19 +111,42 @@ impl MainScreen { } fn next_row(&mut self) { - self.selected_row = std::cmp::min(self.selected_row + 1, self.max_rows); + self.selected_row = std::cmp::min(self.selected_row + 1, self.max_rows - 1); } fn download_row(&mut self) { match self.selected_tab { 1 => {// SC let playlist_url = self.soundcloud.as_ref().unwrap().collection.get(self.selected_row as usize).unwrap().permalink_url.clone(); - dlp::download_from_soundcloud(&playlist_url, &get_temp_dl_dir()); + let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist_url)); }, _ => {} } } + fn render_progress(&self, frame: &mut Frame, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(0), // Main content + Constraint::Length(3), // Progress bar + ]) + .split(area); + + let main_content = Paragraph::new("Main content goes here!") + .block(Block::default().borders(Borders::ALL).title("Main")); + + frame.render_widget(main_content, chunks[0]); + + let gauge = Gauge::default() + .block(Block::default().borders(Borders::ALL).title(" Downloading Playlist ")) + .gauge_style(Style::default().fg(Color::Green)) + .ratio(self.progress.unwrap().0 as f64 / self.progress.unwrap().1 as f64) + .label(format!("{:}/{:}", self.progress.unwrap().0, self.progress.unwrap().1)); + + frame.render_widget(gauge, chunks[1]); + } + fn render_tab(&self) -> Table<'_> { let rows = match self.selected_tab { 1 => { // SC diff --git a/src/sync.rs b/src/sync.rs index f997b77..28757b3 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -5,7 +5,7 @@ use soundcloud::sobjects::CloudPlaylists; use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}, sync::mpsc::{Sender, UnboundedReceiver}}; use tokio_util::sync::CancellationToken; -use crate::config::{get_config_path, get_configs_dir, get_temp_itunesdb, LyricaConfiguration}; +use crate::{config::{get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration}, dlp::{self, DownloadProgress}}; pub enum AppEvent { SearchIPod, @@ -13,7 +13,10 @@ pub enum AppEvent { IPodNotFound, ParseItunes(String), ITunesParsed(XDatabase), - SoundcloudGot(CloudPlaylists) + SoundcloudGot(CloudPlaylists), + DownloadPlaylist(String), + CurrentProgress(DownloadProgress), + OverallProgress((u32, u32)) } pub fn initialize_async_service(sender: Sender, receiver: UnboundedReceiver, token: CancellationToken) { @@ -66,6 +69,9 @@ pub fn initialize_async_service(sender: Sender, receiver: UnboundedRec let _ = sender.send(AppEvent::SoundcloudGot(playlists)).await; }, + AppEvent::DownloadPlaylist(playlist_url) => { + let _ = dlp::download_from_soundcloud(&playlist_url, &get_temp_dl_dir(), sender.clone()).await; + } _ => {} } }