From 3497a54c3921152ab6a454b020b5096f2e856930 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 22 Nov 2024 17:53:09 -0700 Subject: [PATCH] feat(network): Support for triggering an automatic series search in Sonarr --- src/models/sonarr_models.rs | 12 ++++++ src/network/radarr_network.rs | 6 +-- src/network/radarr_network_tests.rs | 4 +- src/network/sonarr_network.rs | 33 +++++++++++++-- src/network/sonarr_network_tests.rs | 64 ++++++++++++++++++++++++++++- 5 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index b215a79..5758bba 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -389,6 +389,18 @@ pub struct SonarrHistoryItem { pub data: SonarrHistoryData, } +#[derive(Default, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SonarrCommandBody { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub series_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub season_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub episode_ids: Option>, +} + #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct SonarrTask { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index dca19e9..6359cd7 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -264,7 +264,7 @@ impl<'a, 'b> Network<'a, 'b> { .await .map(RadarrSerdeable::from), RadarrEvent::TriggerAutomaticSearch(movie_id) => self - .trigger_automatic_search(movie_id) + .trigger_automatic_movie_search(movie_id) .await .map(RadarrSerdeable::from), RadarrEvent::UpdateAllMovies => self.update_all_movies().await.map(RadarrSerdeable::from), @@ -2145,8 +2145,8 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn trigger_automatic_search(&mut self, movie_id: Option) -> Result { - let event = RadarrEvent::TriggerAutomaticSearch(None); + async fn trigger_automatic_movie_search(&mut self, movie_id: Option) -> Result { + let event = RadarrEvent::TriggerAutomaticSearch(movie_id); let (id, _) = self.extract_movie_id(movie_id).await; info!("Searching indexers for movie with ID: {id}"); let body = MovieCommandBody { diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 50e1234..655daa4 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -1162,7 +1162,7 @@ mod test { } #[tokio::test] - async fn test_handle_trigger_automatic_search_event() { + async fn test_handle_trigger_automatic_movie_search_event() { let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ @@ -1194,7 +1194,7 @@ mod test { } #[tokio::test] - async fn test_handle_trigger_automatic_search_event_uses_provided_id() { + async fn test_handle_trigger_automatic_movie_search_event_uses_provided_id() { let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 270f700..f812859 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -19,8 +19,8 @@ use crate::{ }, sonarr_models::{ BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, IndexerSettings, Series, - SonarrHistoryItem, SonarrHistoryWrapper, SonarrSerdeable, SonarrTask, SonarrTaskName, - SystemStatus, + SonarrCommandBody, SonarrHistoryItem, SonarrHistoryWrapper, SonarrSerdeable, SonarrTask, + SonarrTaskName, SystemStatus, }, stateful_table::StatefulTable, HorizontallyScrollableText, Route, Scrollable, ScrollableText, @@ -73,6 +73,7 @@ pub enum SonarrEvent { StartTask(Option), TestIndexer(Option), TestAllIndexers, + TriggerAutomaticSeriesSearch(Option), } impl NetworkResource for SonarrEvent { @@ -91,7 +92,9 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetLogs(_) => "/log", SonarrEvent::GetDiskSpace => "/diskspace", SonarrEvent::GetQualityProfiles => "/qualityprofile", - SonarrEvent::GetQueuedEvents => "/command", + SonarrEvent::GetQueuedEvents + | SonarrEvent::StartTask(_) + | SonarrEvent::TriggerAutomaticSeriesSearch(_) => "/command", SonarrEvent::GetRootFolders | SonarrEvent::DeleteRootFolder(_) | SonarrEvent::AddRootFolder(_) => "/rootfolder", @@ -103,7 +106,6 @@ impl NetworkResource for SonarrEvent { SonarrEvent::HealthCheck => "/health", SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) => "/series", SonarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", - SonarrEvent::StartTask(_) => "/command", SonarrEvent::TestIndexer(_) => "/indexer/test", SonarrEvent::TestAllIndexers => "/indexer/testall", } @@ -240,6 +242,10 @@ impl<'a, 'b> Network<'a, 'b> { .test_all_sonarr_indexers() .await .map(SonarrSerdeable::from), + SonarrEvent::TriggerAutomaticSeriesSearch(series_id) => self + .trigger_automatic_series_search(series_id) + .await + .map(SonarrSerdeable::from), } } @@ -1433,6 +1439,25 @@ impl<'a, 'b> Network<'a, 'b> { .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; + info!("Searching indexers for series with ID: {id}"); + let body = SonarrCommandBody { + name: "SeriesSearch".to_owned(), + series_id: Some(id), + ..SonarrCommandBody::default() + }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + async fn extract_series_id(&mut self, series_id: Option) -> (i64, String) { let series_id = if let Some(id) = series_id { id diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index b7b67e0..e65dd1a 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -163,7 +163,12 @@ mod test { #[rstest] fn test_resource_command( - #[values(SonarrEvent::GetQueuedEvents, SonarrEvent::StartTask(None))] event: SonarrEvent, + #[values( + SonarrEvent::GetQueuedEvents, + SonarrEvent::StartTask(None), + SonarrEvent::TriggerAutomaticSeriesSearch(None) + )] + event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/command"); } @@ -4228,6 +4233,63 @@ mod test { } } + #[tokio::test] + async fn test_handle_trigger_automatic_series_search_event() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Post, + Some(json!({ + "name": "SeriesSearch", + "seriesId": 1 + })), + Some(json!({})), + None, + SonarrEvent::TriggerAutomaticSeriesSearch(None), + None, + None, + ) + .await; + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![series()]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeriesSearch(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_trigger_automatic_series_search_event_uses_provided_id() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Post, + Some(json!({ + "name": "SeriesSearch", + "seriesId": 1 + })), + Some(json!({})), + None, + SonarrEvent::TriggerAutomaticSeriesSearch(None), + None, + None, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(1))) + .await + .is_ok()); + + async_server.assert_async().await; + } + #[tokio::test] async fn test_extract_series_id() { let app_arc = Arc::new(Mutex::new(App::default()));