diff --git a/soulseek-rs-lib/src/client.rs b/soulseek-rs-lib/src/client.rs index fc4263e..3c477a4 100644 --- a/soulseek-rs-lib/src/client.rs +++ b/soulseek-rs-lib/src/client.rs @@ -374,6 +374,10 @@ impl Client { self.context.read().unwrap().searches.clone() } + pub fn get_all_downloads(&self) -> Vec { + self.context.read().unwrap().get_downloads().clone() + } + pub fn download( &self, filename: String, diff --git a/soulseek-rs-lib/src/types.rs b/soulseek-rs-lib/src/types.rs index acac3a4..0536dac 100644 --- a/soulseek-rs-lib/src/types.rs +++ b/soulseek-rs-lib/src/types.rs @@ -98,6 +98,30 @@ pub struct Download { pub sender: Sender, } +impl Download { + pub fn is_finished(&self) -> bool { + matches!( + self.status, + DownloadStatus::Completed | DownloadStatus::Failed | DownloadStatus::TimedOut + ) + } + + pub fn bytes_downloaded(&self) -> u64 { + match &self.status { + DownloadStatus::InProgress { bytes_downloaded, .. } => *bytes_downloaded, + DownloadStatus::Completed => self.size, + _ => 0, + } + } + + pub fn speed_bytes_per_sec(&self) -> f64 { + match &self.status { + DownloadStatus::InProgress { speed_bytes_per_sec, .. } => *speed_bytes_per_sec, + _ => 0.0, + } + } +} + #[derive(Debug, Clone)] #[allow(dead_code)] pub enum DownloadStatus { diff --git a/soulseek-rs/src/models/app_state.rs b/soulseek-rs/src/models/app_state.rs index 9eefa2c..1c4dc40 100644 --- a/soulseek-rs/src/models/app_state.rs +++ b/soulseek-rs/src/models/app_state.rs @@ -1,6 +1,6 @@ -use crate::models::{FileDisplayData, FileDownloadState}; +use crate::models::FileDisplayData; use ratatui::{layout::Rect, widgets::TableState}; -use soulseek_rs::DownloadStatus; +use soulseek_rs::{types::Download, DownloadStatus}; use std::sync::atomic::AtomicBool; use std::sync::{mpsc::Receiver, Arc}; use std::time::Instant; @@ -22,7 +22,7 @@ pub struct SearchEntry { } pub struct DownloadEntry { - pub state: FileDownloadState, + pub download: Download, pub receiver: Option>, } @@ -52,7 +52,7 @@ pub struct AppState { pub downloads: Vec, pub downloads_table_state: TableState, pub downloads_receiver_channel: - Option)>>, + Option)>>, pub active_downloads_count: usize, // UI State diff --git a/soulseek-rs/src/models/download_state.rs b/soulseek-rs/src/models/download_state.rs deleted file mode 100644 index b2af500..0000000 --- a/soulseek-rs/src/models/download_state.rs +++ /dev/null @@ -1,52 +0,0 @@ -use soulseek_rs::DownloadStatus; - -#[derive(Debug, Clone)] -pub struct FileDownloadState { - pub filename: String, - pub username: String, - pub total_bytes: u64, - pub bytes_downloaded: u64, - pub speed_bytes_per_sec: f64, - pub status: DownloadStatus, -} - -impl FileDownloadState { - pub fn new(filename: String, username: String, total_bytes: u64) -> Self { - Self { - filename, - username, - total_bytes, - bytes_downloaded: 0, - speed_bytes_per_sec: 0.0, - status: DownloadStatus::Queued, - } - } - - pub fn update_status(&mut self, status: DownloadStatus) { - match &status { - DownloadStatus::InProgress { - bytes_downloaded, - total_bytes, - speed_bytes_per_sec, - } => { - self.bytes_downloaded = *bytes_downloaded; - self.total_bytes = *total_bytes; - self.speed_bytes_per_sec = *speed_bytes_per_sec; - } - DownloadStatus::Completed => { - self.bytes_downloaded = self.total_bytes; - } - _ => {} - } - self.status = status; - } - - pub fn is_finished(&self) -> bool { - matches!( - self.status, - DownloadStatus::Completed - | DownloadStatus::Failed - | DownloadStatus::TimedOut - ) - } -} diff --git a/soulseek-rs/src/models/mod.rs b/soulseek-rs/src/models/mod.rs index 2a69b0a..caa7b24 100644 --- a/soulseek-rs/src/models/mod.rs +++ b/soulseek-rs/src/models/mod.rs @@ -1,9 +1,7 @@ mod app_state; -mod download_state; mod file_display_data; pub use app_state::{ AppState, DownloadEntry, FocusedPane, SearchEntry, SearchStatus, }; -pub use download_state::FileDownloadState; pub use file_display_data::FileDisplayData; diff --git a/soulseek-rs/src/ui/downloads.rs b/soulseek-rs/src/ui/downloads.rs index 9bc0eb5..8247bee 100644 --- a/soulseek-rs/src/ui/downloads.rs +++ b/soulseek-rs/src/ui/downloads.rs @@ -1,4 +1,4 @@ -use crate::models::FileDownloadState; +use crate::models::DownloadEntry; use crate::ui::{ border_style, border_type, error_style, format_bytes_progress, format_progress_bar, format_shortcuts_styled, format_speed, header_style, @@ -27,9 +27,8 @@ use std::{ const VERSION: &str = env!("CARGO_PKG_VERSION"); pub struct MultiDownloadProgress { - downloads: Vec, - receivers: Vec>>, - receiver_channel: Receiver<(usize, Receiver)>, + downloads: Vec, + receiver_channel: Receiver<(soulseek_rs::types::Download, Receiver)>, list_state: TableState, max_concurrent: usize, active_count: usize, @@ -39,19 +38,14 @@ pub struct MultiDownloadProgress { impl MultiDownloadProgress { pub fn new( - downloads: Vec, - receiver_channel: Receiver<(usize, Receiver)>, + receiver_channel: Receiver<(soulseek_rs::types::Download, Receiver)>, max_concurrent: usize, ) -> Self { let mut list_state = TableState::default(); list_state.select(Some(0)); - // Initialize all receivers as None - they'll be populated asynchronously - let receivers = (0..downloads.len()).map(|_| None).collect(); - Self { - downloads, - receivers, + downloads: Vec::new(), receiver_channel, list_state, max_concurrent, @@ -68,38 +62,39 @@ impl MultiDownloadProgress { loop { terminal.draw(|frame| self.render(frame))?; - // Check for incoming download receivers from background thread - while let Ok((index, receiver)) = self.receiver_channel.try_recv() { - self.receivers[index] = Some(receiver); - // Update status message - let queued = - self.receivers.iter().filter(|r| r.is_none()).count(); - if queued == 0 { - self.queuing_status = String::from("All downloads queued"); - } else { - self.queuing_status = - format!("Queuing... {} remaining", queued); - } + // Check for incoming downloads from background thread + while let Ok((download, receiver)) = self.receiver_channel.try_recv() { + self.downloads.push(DownloadEntry { + download, + receiver: Some(receiver), + }); + self.queuing_status = format!("{} downloads queued", self.downloads.len()); } // Poll all receivers for status updates - for i in 0..self.receivers.len() { - if let Some(receiver) = &self.receivers[i] { + let mut need_start_next = false; + for download_entry in &mut self.downloads { + if let Some(receiver) = &download_entry.receiver { if let Ok(status) = receiver.try_recv() { - let was_active = !self.downloads[i].is_finished(); - self.downloads[i].update_status(status); - let is_finished = self.downloads[i].is_finished(); + let was_active = !download_entry.download.is_finished(); + download_entry.download.status = status; + let is_finished = download_entry.download.is_finished(); - // If download just finished, decrement active count and start next + // If download just finished, decrement active count if was_active && is_finished { self.active_count = self.active_count.saturating_sub(1); - self.start_next_batch(); + need_start_next = true; } } } } + // Start next batch after iteration completes + if need_start_next { + self.start_next_batch(); + } + // Handle keyboard input if poll(Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { @@ -118,14 +113,14 @@ impl MultiDownloadProgress { fn start_next_batch(&mut self) { // Start queued downloads up to max_concurrent limit - for i in 0..self.downloads.len() { + for download_entry in &self.downloads { if self.active_count >= self.max_concurrent { break; } // Only start if download is queued AND receiver is available - if matches!(self.downloads[i].status, DownloadStatus::Queued) - && self.receivers[i].is_some() + if matches!(download_entry.download.status, DownloadStatus::Queued) + && download_entry.receiver.is_some() { self.active_count += 1; // Download will start automatically since receiver exists @@ -134,7 +129,7 @@ impl MultiDownloadProgress { } fn all_finished(&self) -> bool { - self.downloads.iter().all(|d| d.is_finished()) + self.downloads.iter().all(|d| d.download.is_finished()) } fn handle_key(&mut self, key: KeyEvent) { @@ -223,7 +218,8 @@ impl MultiDownloadProgress { let rows: Vec = self .downloads .iter() - .map(|download| { + .map(|download_entry| { + let download = &download_entry.download; let status_icon = match download.status { DownloadStatus::Queued => "⋯", DownloadStatus::InProgress { .. } => "⧗", @@ -232,9 +228,9 @@ impl MultiDownloadProgress { DownloadStatus::TimedOut => "⏱", }; - let progress = if download.total_bytes > 0 { - download.bytes_downloaded as f64 - / download.total_bytes as f64 + let progress = if download.size > 0 { + download.bytes_downloaded() as f64 + / download.size as f64 } else { 0.0 }; @@ -245,13 +241,13 @@ impl MultiDownloadProgress { format_progress_bar(progress, bar_width, percent); let size_str = format_bytes_progress( - download.bytes_downloaded, - download.total_bytes, + download.bytes_downloaded(), + download.size, ); let speed_str = match download.status { DownloadStatus::InProgress { .. } => { - format_speed(download.speed_bytes_per_sec) + format_speed(download.speed_bytes_per_sec()) } _ => "-".to_string(), }; @@ -331,30 +327,30 @@ impl MultiDownloadProgress { pub fn render_download_stats( frame: &mut Frame, area: ratatui::layout::Rect, - downloads: &[FileDownloadState], + downloads: &[DownloadEntry], active_count: usize, ) { let completed = downloads .iter() - .filter(|d| matches!(d.status, DownloadStatus::Completed)) + .filter(|d| matches!(d.download.status, DownloadStatus::Completed)) .count(); let failed = downloads .iter() .filter(|d| { matches!( - d.status, + d.download.status, DownloadStatus::Failed | DownloadStatus::TimedOut ) }) .count(); let queued = downloads .iter() - .filter(|d| matches!(d.status, DownloadStatus::Queued)) + .filter(|d| matches!(d.download.status, DownloadStatus::Queued)) .count(); let total_downloaded: u64 = - downloads.iter().map(|d| d.bytes_downloaded).sum(); - let total_size: u64 = downloads.iter().map(|d| d.total_bytes).sum(); + downloads.iter().map(|d| d.download.bytes_downloaded()).sum(); + let total_size: u64 = downloads.iter().map(|d| d.download.size).sum(); let overall_progress = if total_size > 0 { (total_downloaded as f64 / total_size as f64 * 100.0) as u8 } else { @@ -367,8 +363,8 @@ pub fn render_download_stats( }; let total_speed: f64 = downloads .iter() - .filter(|d| matches!(d.status, DownloadStatus::InProgress { .. })) - .map(|d| d.speed_bytes_per_sec) + .filter(|d| matches!(d.download.status, DownloadStatus::InProgress { .. })) + .map(|d| d.download.speed_bytes_per_sec()) .sum(); let speed_mb = (total_speed / 1_048_576.0 * 100.0).round() / 100.0; @@ -465,45 +461,35 @@ pub fn show_multi_download_progress( ) -> Result<()> { soulseek_rs::utils::logger::enable_buffering(); - // Create download states immediately (all with Queued status) - let downloads: Vec = selected_files - .iter() - .map(|(filename, username, size)| { - FileDownloadState::new(filename.clone(), username.clone(), *size) - }) - .collect(); - - // Create channel for sending receivers from background thread + // Create channel for sending downloads and receivers from background thread let (tx, rx) = mpsc::channel(); // Spawn background thread to initialize downloads let init_client = client.clone(); - let init_files = selected_files.clone(); - let init_dir = download_dir.clone(); thread::spawn(move || { - for (index, (filename, username, size)) in - init_files.into_iter().enumerate() - { + for (filename, username, size) in selected_files.into_iter() { // Initiate download match init_client.download( - filename, - username, + filename.clone(), + username.clone(), size, - init_dir.clone(), + download_dir.clone(), ) { Ok(receiver) => { - // Send receiver back to main thread - let _ = tx.send((index, receiver)); + // Get the download from client + if let Some(download) = init_client.get_all_downloads().iter().find(|d| d.filename == filename && d.username == username).cloned() { + let _ = tx.send((download, receiver)); + } } Err(e) => { - eprintln!("Failed to start download {}: {}", index, e); + eprintln!("Failed to start download for {}: {}", filename, e); } } } }); let mut progress = - MultiDownloadProgress::new(downloads, rx, max_concurrent); + MultiDownloadProgress::new(rx, max_concurrent); let result = progress.run(terminal); ratatui::restore(); diff --git a/soulseek-rs/src/ui/main_tui.rs b/soulseek-rs/src/ui/main_tui.rs index 405a328..23add15 100644 --- a/soulseek-rs/src/ui/main_tui.rs +++ b/soulseek-rs/src/ui/main_tui.rs @@ -1,5 +1,5 @@ use crate::models::{ - AppState, DownloadEntry, FileDisplayData, FileDownloadState, FocusedPane, + AppState, DownloadEntry, FileDisplayData, FocusedPane, SearchEntry, SearchStatus, }; use crate::ui::panes::{ @@ -113,16 +113,10 @@ impl MainTui { }; // Render status bar - let download_states: Vec = self - .state - .downloads - .iter() - .map(|d| d.state.clone()) - .collect(); render_download_stats( frame, main_chunks[0], - &download_states, + &self.state.downloads, self.state.active_downloads_count, ); @@ -184,16 +178,10 @@ impl MainTui { ); // Render Downloads pane - let download_states: Vec = self - .state - .downloads - .iter() - .map(|d| d.state.clone()) - .collect(); render_downloads_pane( frame, right_chunks[1], - &download_states, + &self.state.downloads, &mut self.state.downloads_table_state, self.state.focused_pane == FocusedPane::Downloads, ); @@ -792,27 +780,12 @@ impl MainTui { let (sender, receiver) = mpsc::channel(); self.state.downloads_receiver_channel = Some(receiver); - // Add downloads to state - for (filename, username, size) in &selected_files { - let download_state = FileDownloadState::new( - filename.clone(), - username.clone(), - *size, - ); - self.state.downloads.push(DownloadEntry { - state: download_state, - receiver: None, - }); - } - // Start downloads in background let client = self.client.clone(); let download_dir = self.download_dir.clone(); thread::spawn(move || { - for (idx, (filename, username, size)) in - selected_files.into_iter().enumerate() - { + for (filename, username, size) in selected_files.into_iter() { match client.download( filename.clone(), username.clone(), @@ -820,7 +793,10 @@ impl MainTui { download_dir.clone(), ) { Ok(rx) => { - let _ = sender.send((idx, rx)); + // Get the download from client + if let Some(download) = client.get_all_downloads().iter().find(|d| d.filename == filename && d.username == username).cloned() { + let _ = sender.send((download, rx)); + } } Err(e) => { eprintln!( @@ -837,37 +813,27 @@ impl MainTui { } fn update_downloads(&mut self) { - // Check for new download receivers + // Check for new downloads from background thread if let Some(ref receiver) = self.state.downloads_receiver_channel { - while let Ok((idx, download_receiver)) = receiver.try_recv() { - if let Some(download) = self.state.downloads.get_mut(idx) { - download.receiver = Some(download_receiver); - } + while let Ok((download, download_receiver)) = receiver.try_recv() { + self.state.downloads.push(DownloadEntry { + download, + receiver: Some(download_receiver), + }); } } // Update download states self.state.active_downloads_count = 0; - for download in &mut self.state.downloads { - if let Some(ref receiver) = download.receiver { + for download_entry in &mut self.state.downloads { + if let Some(ref receiver) = download_entry.receiver { while let Ok(status) = receiver.try_recv() { - download.state.status = status.clone(); - if let DownloadStatus::InProgress { - bytes_downloaded, - total_bytes, - speed_bytes_per_sec, - } = &status - { - download.state.bytes_downloaded = *bytes_downloaded; - download.state.total_bytes = *total_bytes; - download.state.speed_bytes_per_sec = - *speed_bytes_per_sec; - } + download_entry.download.status = status; } } if matches!( - download.state.status, + download_entry.download.status, DownloadStatus::InProgress { .. } ) { self.state.active_downloads_count += 1; diff --git a/soulseek-rs/src/ui/panes/downloads_pane.rs b/soulseek-rs/src/ui/panes/downloads_pane.rs index 65ffd01..1ee676f 100644 --- a/soulseek-rs/src/ui/panes/downloads_pane.rs +++ b/soulseek-rs/src/ui/panes/downloads_pane.rs @@ -1,4 +1,4 @@ -use crate::models::FileDownloadState; +use crate::models::DownloadEntry; use crate::ui::{ border_style, border_type, error_style, format_bytes, format_speed, header_style, highlight_style, inactive_style, success_style, @@ -17,7 +17,7 @@ use soulseek_rs::DownloadStatus; pub fn render_downloads_pane( frame: &mut Frame, area: Rect, - downloads: &[FileDownloadState], + downloads: &[DownloadEntry], table_state: &mut TableState, focused: bool, ) { @@ -47,7 +47,8 @@ pub fn render_downloads_pane( let rows: Vec = downloads .iter() - .map(|download| { + .map(|download_entry| { + let download = &download_entry.download; let (status_icon, status_style) = match &download.status { DownloadStatus::Queued => ("⋯", inactive_style()), DownloadStatus::InProgress { .. } => ("⧗", warning_style()), @@ -59,17 +60,17 @@ pub fn render_downloads_pane( let progress_text = match &download.status { DownloadStatus::Queued => "Queued".to_string(), DownloadStatus::InProgress { .. } => { - let percent = if download.total_bytes > 0 { - (download.bytes_downloaded as f64 - / download.total_bytes as f64 + let percent = if download.size > 0 { + (download.bytes_downloaded() as f64 + / download.size as f64 * 100.0) as u8 } else { 0 }; format!( "{}/{} ({}%)", - format_bytes(download.bytes_downloaded), - format_bytes(download.total_bytes), + format_bytes(download.bytes_downloaded()), + format_bytes(download.size), percent ) } @@ -80,7 +81,7 @@ pub fn render_downloads_pane( let speed_text = match &download.status { DownloadStatus::InProgress { .. } => { - format_speed(download.speed_bytes_per_sec) + format_speed(download.speed_bytes_per_sec()) } _ => "-".to_string(), };