From d1ffd0d77fbc5fab7407c02c5e06b00cd0354c2c Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 13 Dec 2024 14:40:11 -0700 Subject: [PATCH] feat(network): Support for toggling the monitoring status of an episode in Sonarr --- src/models/sonarr_models.rs | 7 +++ src/network/sonarr_network.rs | 66 ++++++++++++++++++++- src/network/sonarr_network_tests.rs | 89 ++++++++++++++++++++++++++++- 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 3a9dadf..e2f5c47 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -279,6 +279,13 @@ pub struct MediaInfo { pub subtitles: Option, } +#[derive(Default, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MonitorEpisodeBody { + pub episode_ids: Vec, + pub monitored: bool, +} + #[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq)] #[derivative(Default)] pub struct Rating { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 18b3668..d1fe0cb 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -5,7 +5,7 @@ use serde_json::{json, Value}; use urlencoding::encode; use super::{Network, NetworkEvent, NetworkResource}; -use crate::models::sonarr_models::DownloadStatus; +use crate::models::sonarr_models::{DownloadStatus, MonitorEpisodeBody}; use crate::{ models::{ radarr_models::IndexerTestResult, @@ -88,6 +88,7 @@ pub enum SonarrEvent { TestIndexer(Option), TestAllIndexers, ToggleSeasonMonitoring(Option<(i64, i64)>), + ToggleEpisodeMonitoring(Option), TriggerAutomaticEpisodeSearch(Option), TriggerAutomaticSeasonSearch(Option<(i64, i64)>), TriggerAutomaticSeriesSearch(Option), @@ -146,6 +147,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", SonarrEvent::TestIndexer(_) => "/indexer/test", SonarrEvent::TestAllIndexers => "/indexer/testall", + SonarrEvent::ToggleEpisodeMonitoring(_) => "/episode/monitor", } } } @@ -323,6 +325,10 @@ impl<'a, 'b> Network<'a, 'b> { .test_all_sonarr_indexers() .await .map(SonarrSerdeable::from), + SonarrEvent::ToggleEpisodeMonitoring(episode_id) => self + .toggle_sonarr_episode_monitoring(episode_id) + .await + .map(SonarrSerdeable::from), SonarrEvent::ToggleSeasonMonitoring(params) => self .toggle_sonarr_season_monitoring(params) .await @@ -2532,6 +2538,64 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn toggle_sonarr_episode_monitoring(&mut self, episode_id: Option) -> Result { + let event = SonarrEvent::ToggleEpisodeMonitoring(episode_id); + let detail_event = SonarrEvent::GetEpisodeDetails(None); + + let (id, monitored) = if let Some(episode_id) = episode_id { + info!("Fetching episode details for episode id: {episode_id}"); + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{episode_id}")), + None, + ) + .await; + + let mut monitored = false; + + self + .handle_request::<(), Value>(request_props, |detailed_episode_body, _| { + monitored = detailed_episode_body + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); + }) + .await?; + + (episode_id, monitored) + } else { + let app = self.app.lock().await; + let current_selection = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episodes + .current_selection(); + (current_selection.id, current_selection.monitored) + }; + + info!("Toggling monitoring for episode id: {id}"); + + let body = MonitorEpisodeBody { + episode_ids: vec![id], + monitored: !monitored, + }; + + let request_props = self + .request_props_from(event, RequestMethod::Put, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + async fn trigger_automatic_series_search(&mut self, series_id: Option) -> Result { let event = SonarrEvent::TriggerAutomaticSeriesSearch(series_id); let (id, _) = self.extract_series_id(series_id).await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index f8f8b06..2c9800d 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -17,7 +17,8 @@ mod test { use crate::models::sonarr_models::{ AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, AddSeriesSearchResultStatistics, - DownloadStatus, EditSeriesParams, IndexerSettings, SeriesMonitor, SonarrHistoryEventType, + DownloadStatus, EditSeriesParams, IndexerSettings, MonitorEpisodeBody, SeriesMonitor, + SonarrHistoryEventType, }; use crate::app::{App, ServarrConfig}; @@ -294,6 +295,7 @@ mod test { #[case(SonarrEvent::SearchNewSeries(None), "/series/lookup")] #[case(SonarrEvent::TestIndexer(None), "/indexer/test")] #[case(SonarrEvent::TestAllIndexers, "/indexer/testall")] + #[case(SonarrEvent::ToggleEpisodeMonitoring(None), "/episode/monitor")] fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) { assert_str_eq!(event.resource(), expected_uri); } @@ -6641,6 +6643,91 @@ mod test { } } + #[tokio::test] + async fn test_handle_toggle_episode_monitoring_event() { + let expected_body = MonitorEpisodeBody { + episode_ids: vec![1], + monitored: false, + }; + + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Put, + Some(json!(expected_body)), + Some(json!({})), + None, + SonarrEvent::ToggleEpisodeMonitoring(None), + None, + None, + ) + .await; + { + let mut app = app_arc.lock().await; + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ToggleEpisodeMonitoring(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_toggle_episode_monitoring_event_uses_provided_episode_id() { + let expected_body = MonitorEpisodeBody { + episode_ids: vec![2], + monitored: false, + }; + let body = Episode { + id: 2, + ..episode() + }; + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!(body)), + None, + SonarrEvent::GetEpisodeDetails(None), + Some("/2"), + None, + ) + .await; + let async_toggle_server = server + .mock( + "PUT", + format!( + "/api/v3{}", + SonarrEvent::ToggleEpisodeMonitoring(None).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(json!(expected_body))) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ToggleEpisodeMonitoring(Some(2))) + .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() { let mut expected_body: Value = serde_json::from_str(SERIES_JSON).unwrap();