diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 5d0a622..18b3668 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -87,6 +87,7 @@ pub enum SonarrEvent { StartTask(Option), TestIndexer(Option), TestAllIndexers, + ToggleSeasonMonitoring(Option<(i64, i64)>), TriggerAutomaticEpisodeSearch(Option), TriggerAutomaticSeasonSearch(Option<(i64, i64)>), TriggerAutomaticSeriesSearch(Option), @@ -139,7 +140,8 @@ impl NetworkResource for SonarrEvent { | SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) | SonarrEvent::DeleteSeries(_) - | SonarrEvent::EditSeries(_) => "/series", + | SonarrEvent::EditSeries(_) + | SonarrEvent::ToggleSeasonMonitoring(_) => "/series", SonarrEvent::SearchNewSeries(_) => "/series/lookup", SonarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", SonarrEvent::TestIndexer(_) => "/indexer/test", @@ -321,6 +323,10 @@ impl<'a, 'b> Network<'a, 'b> { .test_all_sonarr_indexers() .await .map(SonarrSerdeable::from), + SonarrEvent::ToggleSeasonMonitoring(params) => self + .toggle_sonarr_season_monitoring(params) + .await + .map(SonarrSerdeable::from), SonarrEvent::TriggerAutomaticSeasonSearch(params) => self .trigger_automatic_season_search(params) .await @@ -1268,6 +1274,86 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn toggle_sonarr_season_monitoring( + &mut self, + series_id_season_number_tuple: Option<(i64, i64)>, + ) -> Result<()> { + let detail_event = SonarrEvent::GetSeriesDetails(None); + let event = SonarrEvent::ToggleSeasonMonitoring(series_id_season_number_tuple); + let (series_id, season_number) = + if let Some((series_id, season_number)) = series_id_season_number_tuple { + (Some(series_id), Some(season_number)) + } else { + (None, None) + }; + + 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}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{series_id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_series_body, _| { + response = detailed_series_body.to_string() + }) + .await?; + + 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(); + + *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:?}"); + + 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 + } + async fn get_all_sonarr_indexer_settings(&mut self) -> Result { info!("Fetching Sonarr indexer settings"); let event = SonarrEvent::GetAllIndexerSettings; @@ -1563,7 +1649,7 @@ impl<'a, 'b> Network<'a, 'b> { .unwrap() .episode_details_modal = Some(EpisodeDetailsModal::default()); } - + let Episode { id, title, @@ -1581,18 +1667,26 @@ impl<'a, 'b> Network<'a, 'b> { } else { String::new() }; - let episode_details_modal = app.data.sonarr_data.season_details_modal.as_mut().unwrap().episode_details_modal.as_mut().unwrap(); - episode_details_modal.episode_details = ScrollableText::with_string(formatdoc!( - " + let episode_details_modal = app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap(); + episode_details_modal.episode_details = ScrollableText::with_string(formatdoc!( + " Title: {} Season: {season_number} Episode Number: {episode_number} Air Date: {air_date} Status: {status} Description: {}", - title, - overview.unwrap_or_default(), - )); + title, + overview.unwrap_or_default(), + )); if let Some(file) = episode_file { let size = convert_to_gb(file.size); episode_details_modal.file_details = formatdoc!( diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 31512f1..f8f8b06 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -162,7 +162,8 @@ mod test { SonarrEvent::ListSeries, SonarrEvent::GetSeriesDetails(None), SonarrEvent::DeleteSeries(None), - SonarrEvent::EditSeries(None) + SonarrEvent::EditSeries(None), + SonarrEvent::ToggleSeasonMonitoring(None) )] event: SonarrEvent, ) { @@ -6640,6 +6641,125 @@ mod test { } } + #[tokio::test] + async fn test_handle_toggle_season_monitoring_event() { + let mut expected_body: Value = serde_json::from_str(SERIES_JSON).unwrap(); + *expected_body + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == 1) + .unwrap() + .get_mut("monitored") + .unwrap() = json!(false); + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(SERIES_JSON).unwrap()), + None, + SonarrEvent::GetSeriesDetails(None), + Some("/1"), + None, + ) + .await; + let async_toggle_server = server + .mock( + "PUT", + format!( + "/api/v3{}/1", + SonarrEvent::ToggleSeasonMonitoring(None).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.sonarr_data.series.set_items(vec![series()]); + app.data.sonarr_data.seasons.set_items(vec![season()]); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring(None)) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_toggle_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_toggle_season_monitoring_event_uses_provided_series_id_and_season_number() { + let mut detailed_response: Value = serde_json::from_str(SERIES_JSON).unwrap(); + *detailed_response + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == 1) + .unwrap() + .get_mut("seasonNumber") + .unwrap() = json!(2); + let mut expected_body: Value = detailed_response.clone(); + *expected_body + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == 2) + .unwrap() + .get_mut("monitored") + .unwrap() = json!(false); + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(detailed_response), + None, + SonarrEvent::GetSeriesDetails(None), + Some("/2"), + None, + ) + .await; + let async_toggle_server = server + .mock( + "PUT", + format!( + "/api/v3{}/2", + SonarrEvent::ToggleSeasonMonitoring(Some((2, 2))).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.sonarr_data.series.set_items(vec![series()]); + app.data.sonarr_data.seasons.set_items(vec![season()]); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring(Some((2, 2)))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_toggle_server.assert_async().await; + } + #[tokio::test] async fn test_handle_trigger_automatic_episode_search_event() { let (async_server, app_arc, _server) = mock_servarr_api(