diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 8fc0a0a..11ddd4d 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -165,7 +165,7 @@ impl<'a> App<'a> { } if self.is_routing { - if self.is_loading && !self.should_refresh { + if !self.should_refresh { self.cancellation_token.cancel(); } diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 4dbd9f1..90a1d97 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -582,7 +582,6 @@ mod tests { async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { let (mut app, mut sync_network_rx) = construct_app_unit(); app.is_routing = true; - app.is_loading = true; app.should_refresh = false; app diff --git a/src/main.rs b/src/main.rs index 36af21f..e4281ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::panic::PanicHookInfo; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::time::Duration; use std::{io, panic, process}; use anyhow::anyhow; @@ -20,11 +21,12 @@ use crossterm::execute; use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; -use log::error; +use log::{error, warn}; use network::NetworkTrait; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use reqwest::{Certificate, Client}; +use tokio::select; use tokio::sync::mpsc::Receiver; use tokio::sync::{mpsc, Mutex}; use tokio_util::sync::CancellationToken; @@ -144,9 +146,24 @@ async fn start_networking( ) { let mut network = Network::new(app, cancellation_token, client); - while let Some(network_event) = network_rx.recv().await { - if let Err(e) = network.handle_network_event(network_event).await { - error!("Encountered an error handling network event: {e:?}"); + loop { + select! { + Some(network_event) = network_rx.recv() => { + if let Err(e) = network.handle_network_event(network_event).await { + error!("Encountered an error handling network event: {e:?}"); + } + } + _ = network.cancellation_token.cancelled() => { + warn!("Clearing network channel"); + while network_rx.try_recv().is_ok() { + // Discard the message + } + { + /* Wrapped in its own block so we don't lock the app arc early, + so UI is still processed */ + network.reset_cancellation_token().await; + } + } } } } @@ -228,7 +245,10 @@ fn load_config(path: &str) -> Result { } fn build_network_client(config: &AppConfig) -> Client { - let mut client_builder = Client::builder(); + let mut client_builder = Client::builder() + .pool_max_idle_per_host(10) + .http2_keep_alive_interval(Duration::from_secs(5)) + .tcp_keepalive(Duration::from_secs(5)); if let Some(ref cert_path) = config.radarr.ssl_cert_path { let cert = create_cert(cert_path, "Radarr"); diff --git a/src/network/mod.rs b/src/network/mod.rs index 135a3cf..da944e3 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -40,7 +40,7 @@ pub trait NetworkTrait { #[derive(Clone)] pub struct Network<'a, 'b> { client: Client, - cancellation_token: CancellationToken, + pub cancellation_token: CancellationToken, pub app: &'a Arc>>, } @@ -74,6 +74,13 @@ impl<'a, 'b> Network<'a, 'b> { } } + pub(super) async fn reset_cancellation_token(&mut self) { + let mut app = self.app.lock().await; + self.cancellation_token = app.reset_cancellation_token(); + app.should_refresh = true; + app.is_loading = false; + } + async fn handle_request( &mut self, request_props: RequestProps, @@ -89,9 +96,6 @@ impl<'a, 'b> Network<'a, 'b> { select! { _ = self.cancellation_token.cancelled() => { warn!("Received Cancel request. Cancelling request to: {request_uri}"); - let mut app = self.app.lock().await; - self.cancellation_token = app.reset_cancellation_token(); - app.is_loading = false; Ok(R::default()) } resp = self.call_api(request_props).await.send() => { diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index e26f8af..5528a0f 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -181,11 +181,31 @@ mod tests { assert!(!async_server.matched_async().await); assert!(app_arc.lock().await.error.text.is_empty()); - assert!(!network.cancellation_token.is_cancelled()); assert!(resp.is_ok()); assert_eq!(resp.unwrap(), Test::default()); } + #[tokio::test] + async fn test_reset_cancellation_token() { + let cancellation_token = CancellationToken::new(); + let (tx, _) = mpsc::channel::(500); + let app_arc = Arc::new(Mutex::new(App::new( + tx, + AppConfig::default(), + cancellation_token.clone(), + ))); + app_arc.lock().await.should_refresh = false; + app_arc.lock().await.is_loading = true; + let mut network = Network::new(&app_arc, cancellation_token, Client::new()); + network.cancellation_token.cancel(); + + network.reset_cancellation_token().await; + + assert!(!network.cancellation_token.is_cancelled()); + assert!(app_arc.lock().await.should_refresh); + assert!(!app_arc.lock().await.is_loading); + } + #[tokio::test] async fn test_handle_request_get_invalid_body() { let mut server = Server::new_async().await; diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index 7174913..b8218bf 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -286,83 +286,88 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let is_loading = app.is_loading && app.data.radarr_data.movie_details_modal.is_none(); - let cast_row_mapping = |cast_member: &Credit| { - let Credit { - person_name, - character, - .. - } = cast_member; + match app.data.radarr_data.movie_details_modal.as_mut() { + Some(movie_details_modal) if !app.is_loading => { + let cast_row_mapping = |cast_member: &Credit| { + let Credit { + person_name, + character, + .. + } = cast_member; - Row::new(vec![ - Cell::from(person_name.to_owned()), - Cell::from(character.clone().unwrap_or_default()), - ]) - .success() - }; - let content = Some( - &mut app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_cast, - ); - let help_footer = app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(); - let cast_table = ManagarrTable::new(content, cast_row_mapping) - .block(layout_block_top_border()) - .footer(help_footer) - .loading(is_loading) - .headers(["Cast Member", "Character"]) - .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); + Row::new(vec![ + Cell::from(person_name.to_owned()), + Cell::from(character.clone().unwrap_or_default()), + ]) + .success() + }; + let content = Some(&mut movie_details_modal.movie_cast); + let help_footer = app + .data + .radarr_data + .movie_info_tabs + .get_active_tab_contextual_help(); + let cast_table = ManagarrTable::new(content, cast_row_mapping) + .block(layout_block_top_border()) + .footer(help_footer) + .loading(app.is_loading) + .headers(["Cast Member", "Character"]) + .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); - f.render_widget(cast_table, area); + f.render_widget(cast_table, area); + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.radarr_data.movie_details_modal.is_none(), + layout_block_top_border(), + ), + area, + ), + } } fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let is_loading = app.is_loading && app.data.radarr_data.movie_details_modal.is_none(); - let crew_row_mapping = |crew_member: &Credit| { - let Credit { - person_name, - job, - department, - .. - } = crew_member; + match app.data.radarr_data.movie_details_modal.as_mut() { + Some(movie_details_modal) if !app.is_loading => { + let crew_row_mapping = |crew_member: &Credit| { + let Credit { + person_name, + job, + department, + .. + } = crew_member; - Row::new(vec![ - Cell::from(person_name.to_owned()), - Cell::from(job.clone().unwrap_or_default()), - Cell::from(department.clone().unwrap_or_default()), - ]) - .success() - }; - let content = Some( - &mut app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_crew, - ); - let help_footer = app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(); - let crew_table = ManagarrTable::new(content, crew_row_mapping) - .block(layout_block_top_border()) - .loading(is_loading) - .headers(["Crew Member", "Job", "Department"]) - .constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3)) - .footer(help_footer); + Row::new(vec![ + Cell::from(person_name.to_owned()), + Cell::from(job.clone().unwrap_or_default()), + Cell::from(department.clone().unwrap_or_default()), + ]) + .success() + }; + let content = Some(&mut movie_details_modal.movie_crew); + let help_footer = app + .data + .radarr_data + .movie_info_tabs + .get_active_tab_contextual_help(); + let crew_table = ManagarrTable::new(content, crew_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .headers(["Crew Member", "Job", "Department"]) + .constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3)) + .footer(help_footer); - f.render_widget(crew_table, area); + f.render_widget(crew_table, area); + } + + _ => f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.radarr_data.movie_details_modal.is_none(), + layout_block_top_border(), + ), + area, + ), + } } fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {