From 9269b66aa8f305ff3a84e8fa51ac8f8722758292 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 13 Dec 2024 16:10:06 -0700 Subject: [PATCH] feat(handlers): Support for toggling the monitoring status of a season in the Sonarr UI --- src/app/key_binding_tests.rs | 2 +- src/app/sonarr/mod.rs | 3 + src/app/sonarr/sonarr_tests.rs | 6 +- .../library/series_details_handler.rs | 7 + .../library/series_details_handler_tests.rs | 50 ++++++ src/network/sonarr_network.rs | 143 +++++++++--------- src/network/sonarr_network_tests.rs | 20 ++- 7 files changed, 158 insertions(+), 73 deletions(-) diff --git a/src/app/key_binding_tests.rs b/src/app/key_binding_tests.rs index ac2231c..c78c210 100644 --- a/src/app/key_binding_tests.rs +++ b/src/app/key_binding_tests.rs @@ -27,7 +27,7 @@ mod test { #[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), "tasks")] #[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), "test")] #[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), "test all")] - #[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('m'), "toggle monitoring")] + #[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), "toggle monitoring")] #[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), "refresh")] #[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), "update")] #[case(DEFAULT_KEYBINDINGS.home, Key::Home, "home")] diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 30f042a..3d2bb82 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -29,6 +29,9 @@ impl<'a> App<'a> { .await; } ActiveSonarrBlock::SeriesDetails => { + self + .dispatch_network_event(SonarrEvent::ListSeries.into()) + .await; self.is_loading = true; self.populate_seasons_table().await; self.is_loading = false; diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index df7b438..8237f7a 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -56,7 +56,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_series_details_block() { - let (mut app, _) = construct_app_unit(); + let (mut app, mut sync_network_rx) = construct_app_unit(); app.data.sonarr_data.series.set_items(vec![Series { seasons: Some(vec![Season::default()]), @@ -68,6 +68,10 @@ mod tests { .await; assert!(!app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::ListSeries.into() + ); assert!(!app.data.sonarr_data.seasons.items.is_empty()); assert_eq!(app.tick_count, 0); assert!(!app.data.sonarr_data.prompt_confirm); diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs index 66bc3c8..00224b4 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -257,6 +257,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler self.app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); } + _ if key == DEFAULT_KEYBINDINGS.toggle_monitoring.key => { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::ToggleSeasonMonitoring(None)); + + self.app.pop_and_push_navigation_stack(self.active_sonarr_block.into()); + } _ => (), }, ActiveSonarrBlock::SeriesHistory => match self.key { diff --git a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs index 76e4244..20437ee 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs @@ -375,6 +375,56 @@ mod tests { assert!(app.data.sonarr_data.edit_series_modal.is_none()); } + #[test] + fn test_toggle_monitoring_key() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.is_routing = false; + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.is_routing); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::ToggleSeasonMonitoring(None)) + ); + } + + #[test] + fn test_toggle_monitoring_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.is_routing = false; + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::SeriesDetails.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.prompt_confirm_action.is_none()); + assert!(!app.is_routing); + } + #[rstest] fn test_auto_search_key( #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 2656c63..112486c 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -879,7 +879,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit indexer body"); - let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap(); + let mut detailed_indexer_body: Value = serde_json::from_str(&response)?; let ( name, @@ -1127,7 +1127,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit series body"); - let mut detailed_series_body: Value = serde_json::from_str(&response).unwrap(); + let mut detailed_series_body: Value = serde_json::from_str(&response)?; let ( monitored, use_season_folders, @@ -1294,70 +1294,75 @@ impl<'a, 'b> Network<'a, 'b> { }; let (series_id, _) = self.extract_series_id(series_id).await; - let (season_number, _) = self.extract_season_number(season_number).await; - info!("Toggling season monitoring for season {season_number} in series with ID: {series_id}"); - info!("Fetching series details for series with ID: {series_id}"); + if let Ok((season_number, _)) = self.extract_season_number(season_number).await { + info!("Toggling season monitoring for season {season_number} in series with ID: {series_id}"); + info!("Fetching series details for series with ID: {series_id}"); - let request_props = self - .request_props_from( - detail_event, - RequestMethod::Get, - None::<()>, - Some(format!("/{series_id}")), - None, - ) - .await; + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{series_id}")), + None, + ) + .await; - let mut response = String::new(); + let mut response = String::new(); - self - .handle_request::<(), Value>(request_props, |detailed_series_body, _| { - response = detailed_series_body.to_string() - }) - .await?; + self + .handle_request::<(), Value>(request_props, |detailed_series_body, _| { + response = detailed_series_body.to_string() + }) + .await?; - info!("Constructing toggle season monitoring body"); + info!("Constructing toggle season monitoring body"); - let mut detailed_series_body: Value = serde_json::from_str(&response).unwrap(); - let monitored = detailed_series_body - .get("seasons") - .unwrap() - .as_array() - .unwrap() - .iter() - .find(|season| season["seasonNumber"] == season_number) - .unwrap() - .get("monitored") - .unwrap() - .as_bool() - .unwrap(); + let mut detailed_series_body: Value = + serde_json::from_str(&response).expect("Request for detailed series body was interrupted"); + let monitored = detailed_series_body + .get("seasons") + .unwrap() + .as_array() + .unwrap() + .iter() + .find(|season| season["seasonNumber"] == season_number) + .unwrap() + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); - *detailed_series_body - .get_mut("seasons") - .unwrap() - .as_array_mut() - .unwrap() - .iter_mut() - .find(|season| season["seasonNumber"] == season_number) - .unwrap() - .get_mut("monitored") - .unwrap() = json!(!monitored); + *detailed_series_body + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == season_number) + .unwrap() + .get_mut("monitored") + .unwrap() = json!(!monitored); - debug!("Toggle season monitoring body: {detailed_series_body:?}"); + debug!("Toggle season monitoring body: {detailed_series_body:?}"); - let request_props = self - .request_props_from( - event, - RequestMethod::Put, - Some(detailed_series_body), - Some(format!("/{series_id}")), - None, - ) - .await; + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_series_body), + Some(format!("/{series_id}")), + None, + ) + .await; - self - .handle_request::(request_props, |_, _| ()) - .await + self + .handle_request::(request_props, |_, _| ()) + .await + } else { + warn!("Season number was not provided. Aborting..."); + Ok(()) + } } async fn get_all_sonarr_indexer_settings(&mut self) -> Result { @@ -2003,7 +2008,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let (series_id, series_id_param) = self.extract_series_id(series_id).await; - let (season_number, season_number_param) = self.extract_season_number(season_number).await; + let (season_number, season_number_param) = self.extract_season_number(season_number).await?; info!("Fetching releases for series with ID: {series_id} and season number: {season_number}"); @@ -2053,7 +2058,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let (series_id, series_id_param) = self.extract_series_id(series_id).await; - let (season_number, season_number_param) = self.extract_season_number(season_number).await; + let (season_number, season_number_param) = self.extract_season_number(season_number).await?; info!("Fetching history for series with ID: {series_id} and season number: {season_number}"); @@ -2629,7 +2634,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let (series_id, _) = self.extract_series_id(series_id).await; - let (season_number, _) = self.extract_season_number(season_number).await; + let (season_number, _) = self.extract_season_number(season_number).await?; info!("Searching indexers for series with ID: {series_id} and season number: {season_number}"); let body = SonarrCommandBody { @@ -2767,11 +2772,11 @@ impl<'a, 'b> Network<'a, 'b> { (series_id, format!("seriesId={series_id}")) } - async fn extract_season_number(&mut self, season_number: Option) -> (i64, String) { - let season_number = if let Some(number) = season_number { - number - } else { - self + async fn extract_season_number(&mut self, season_number: Option) -> Result<(i64, String)> { + if let Some(number) = season_number { + Ok((number, format!("seasonNumber={number}"))) + } else if !self.app.lock().await.data.sonarr_data.seasons.is_empty() { + let season_number = self .app .lock() .await @@ -2779,9 +2784,11 @@ impl<'a, 'b> Network<'a, 'b> { .sonarr_data .seasons .current_selection() - .season_number - }; - (season_number, format!("seasonNumber={season_number}")) + .season_number; + Ok((season_number, format!("seasonNumber={season_number}"))) + } else { + Err(anyhow!("No season number provided")) + } } async fn extract_episode_id(&mut self, episode_id: Option) -> i64 { diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 1ae375f..39ebc16 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -5129,12 +5129,14 @@ mod test { ) .await; let mut filtered_series = StatefulTable::default(); + filtered_series.set_items(vec![Series::default()]); filtered_series.set_filtered_items(vec![Series { id: 1, ..Series::default() }]); app_arc.lock().await.data.sonarr_data.series = filtered_series; let mut filtered_seasons = StatefulTable::default(); + filtered_seasons.set_items(vec![Season::default()]); filtered_seasons.set_filtered_items(vec![Season { season_number: 1, ..Season::default() @@ -7024,12 +7026,14 @@ mod test { ) .await; let mut filtered_series = StatefulTable::default(); + filtered_series.set_items(vec![Series::default()]); filtered_series.set_filtered_items(vec![Series { id: 1, ..Series::default() }]); app_arc.lock().await.data.sonarr_data.series = filtered_series; let mut filtered_seasons = StatefulTable::default(); + filtered_seasons.set_items(vec![Season::default()]); filtered_seasons.set_filtered_items(vec![Season { season_number: 1, ..Season::default() @@ -7341,7 +7345,7 @@ mod test { }]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let (id, season_number_param) = network.extract_season_number(None).await; + let (id, season_number_param) = network.extract_season_number(None).await.unwrap(); assert_eq!(id, 1); assert_str_eq!(season_number_param, "seasonNumber=1"); @@ -7361,7 +7365,7 @@ mod test { ..Season::default() }]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let (id, season_number_param) = network.extract_season_number(Some(2)).await; + let (id, season_number_param) = network.extract_season_number(Some(2)).await.unwrap(); assert_eq!(id, 2); assert_str_eq!(season_number_param, "seasonNumber=2"); @@ -7371,6 +7375,7 @@ mod test { async fn test_extract_season_number_filtered_seasons() { let app_arc = Arc::new(Mutex::new(App::default())); let mut filtered_seasons = StatefulTable::default(); + filtered_seasons.set_items(vec![Season::default()]); filtered_seasons.set_filtered_items(vec![Season { season_number: 1, ..Season::default() @@ -7378,12 +7383,21 @@ mod test { app_arc.lock().await.data.sonarr_data.seasons = filtered_seasons; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let (id, season_number_param) = network.extract_season_number(None).await; + let (id, season_number_param) = network.extract_season_number(None).await.unwrap(); assert_eq!(id, 1); assert_str_eq!(season_number_param, "seasonNumber=1"); } + #[tokio::test] + async fn test_extract_season_number_empty_seasons_table() { + let app_arc = Arc::new(Mutex::new(App::default())); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + let season_number = network.extract_season_number(None).await; + + assert!(season_number.is_err()); + } + #[tokio::test] async fn test_extract_episode_id() { let app_arc = Arc::new(Mutex::new(App::default()));