From cac54c544763f8f96d4f84e81cfe4106147dd252 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 23 Nov 2024 12:42:11 -0700 Subject: [PATCH] feat(network): Support for deleting a series from Sonarr --- src/models/servarr_data/sonarr/sonarr_data.rs | 13 ++++ .../servarr_data/sonarr/sonarr_data_tests.rs | 16 +++++ src/models/sonarr_models.rs | 8 +++ src/network/sonarr_network.rs | 64 +++++++++++++++-- src/network/sonarr_network_tests.rs | 70 ++++++++++++++++++- 5 files changed, 164 insertions(+), 7 deletions(-) diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 6d44336..ea91074 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -20,7 +20,9 @@ use super::modals::SeasonDetailsModal; mod sonarr_data_tests; pub struct SonarrData { + pub add_list_exclusion: bool, pub blocklist: StatefulTable, + pub delete_series_files: bool, pub downloads: StatefulTable, pub disk_space_vec: Vec, pub edit_root_folder: Option, @@ -44,11 +46,20 @@ pub struct SonarrData { pub version: String, } +impl SonarrData { + pub fn reset_delete_series_preferences(&mut self) { + self.delete_series_files = false; + self.add_list_exclusion = false; + } +} + impl Default for SonarrData { fn default() -> SonarrData { SonarrData { + add_list_exclusion: false, blocklist: StatefulTable::default(), downloads: StatefulTable::default(), + delete_series_files: false, disk_space_vec: Vec::new(), edit_root_folder: None, history: StatefulTable::default(), @@ -97,6 +108,8 @@ pub enum ActiveSonarrBlock { DeleteRootFolderPrompt, DeleteSeriesConfirmPrompt, DeleteSeriesPrompt, + DeleteSeriesToggleAddListExclusion, + DeleteSeriesToggleDeleteFile, Downloads, EditEpisodePrompt, EditIndexerPrompt, diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index b17150a..593cbf3 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -30,11 +30,27 @@ mod tests { ); } + #[test] + fn test_reset_delete_series_preferences() { + let mut sonarr_data = SonarrData { + add_list_exclusion: true, + delete_series_files: true, + ..SonarrData::default() + }; + + sonarr_data.reset_delete_series_preferences(); + + assert!(!sonarr_data.delete_series_files); + assert!(!sonarr_data.add_list_exclusion); + } + #[test] fn test_sonarr_data_defaults() { let sonarr_data = SonarrData::default(); + assert!(!sonarr_data.add_list_exclusion); assert!(sonarr_data.blocklist.is_empty()); + assert!(!sonarr_data.delete_series_files); assert!(sonarr_data.downloads.is_empty()); assert!(sonarr_data.disk_space_vec.is_empty()); assert!(sonarr_data.edit_root_folder.is_none()); diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index ac253f8..caac288 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -44,6 +44,14 @@ pub struct BlocklistResponse { pub records: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub struct DeleteSeriesParams { + pub id: i64, + pub delete_series_files: bool, + pub add_list_exclusion: bool, +} + #[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DownloadRecord { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index ae96b8f..15a221b 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -18,9 +18,10 @@ use crate::{ QueueEvent, RootFolder, SecurityConfig, Tag, Update, }, sonarr_models::{ - BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, IndexerSettings, Series, - SonarrCommandBody, SonarrHistoryItem, SonarrHistoryWrapper, SonarrRelease, - SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, SonarrTaskName, SystemStatus, + BlocklistResponse, DeleteSeriesParams, DownloadRecord, DownloadsResponse, Episode, + IndexerSettings, Series, SonarrCommandBody, SonarrHistoryItem, SonarrHistoryWrapper, + SonarrRelease, SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, SonarrTaskName, + SystemStatus, }, stateful_table::StatefulTable, HorizontallyScrollableText, Route, Scrollable, ScrollableText, @@ -43,6 +44,7 @@ pub enum SonarrEvent { DeleteDownload(Option), DeleteIndexer(Option), DeleteRootFolder(Option), + DeleteSeries(Option), DeleteTag(i64), DownloadRelease(SonarrReleaseDownloadBody), GetAllIndexerSettings, @@ -116,7 +118,9 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetTasks => "/system/task", SonarrEvent::GetUpdates => "/update", SonarrEvent::HealthCheck => "/health", - SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) => "/series", + SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) | SonarrEvent::DeleteSeries(_) => { + "/series" + } SonarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", SonarrEvent::TestIndexer(_) => "/indexer/test", SonarrEvent::TestAllIndexers => "/indexer/testall", @@ -165,6 +169,9 @@ impl<'a, 'b> Network<'a, 'b> { .delete_sonarr_root_folder(root_folder_id) .await .map(SonarrSerdeable::from), + SonarrEvent::DeleteSeries(params) => { + self.delete_series(params).await.map(SonarrSerdeable::from) + } SonarrEvent::DeleteTag(tag_id) => self .delete_sonarr_tag(tag_id) .await @@ -498,6 +505,55 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn delete_series( + &mut self, + delete_series_params: Option, + ) -> Result<()> { + let event = SonarrEvent::DeleteSeries(None); + let (series_id, delete_files, add_import_exclusion) = if let Some(params) = delete_series_params + { + ( + params.id, + params.delete_series_files, + params.add_list_exclusion, + ) + } else { + let (series_id, _) = self.extract_series_id(None).await; + let delete_files = self.app.lock().await.data.sonarr_data.delete_series_files; + let add_import_exclusion = self.app.lock().await.data.sonarr_data.add_list_exclusion; + + (series_id, delete_files, add_import_exclusion) + }; + + info!("Deleting Sonarr series with ID: {series_id} with deleteFiles={delete_files} and addImportExclusion={add_import_exclusion}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{series_id}")), + Some(format!( + "deleteFiles={delete_files}&addImportExclusion={add_import_exclusion}" + )), + ) + .await; + + let resp = self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await; + + self + .app + .lock() + .await + .data + .sonarr_data + .reset_delete_series_preferences(); + + resp + } + async fn delete_sonarr_tag(&mut self, id: i64) -> Result<()> { info!("Deleting Sonarr tag with id: {id}"); let event = SonarrEvent::DeleteTag(id); diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index f0c7d88..b7d0625 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -24,8 +24,8 @@ mod test { QualityWrapper, QueueEvent, RootFolder, SecurityConfig, Tag, Update, }; use crate::models::sonarr_models::{ - BlocklistItem, DownloadRecord, DownloadsResponse, Episode, EpisodeFile, MediaInfo, - SonarrRelease, SonarrReleaseDownloadBody, SonarrTaskName, + BlocklistItem, DeleteSeriesParams, DownloadRecord, DownloadsResponse, Episode, EpisodeFile, + MediaInfo, SonarrRelease, SonarrReleaseDownloadBody, SonarrTaskName, }; use crate::models::sonarr_models::{ BlocklistResponse, SonarrHistoryData, SonarrHistoryItem, SonarrHistoryWrapper, @@ -137,7 +137,12 @@ mod test { #[rstest] fn test_resource_series( - #[values(SonarrEvent::ListSeries, SonarrEvent::GetSeriesDetails(None))] event: SonarrEvent, + #[values( + SonarrEvent::ListSeries, + SonarrEvent::GetSeriesDetails(None), + SonarrEvent::DeleteSeries(None) + )] + event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/series"); } @@ -577,6 +582,65 @@ mod test { async_server.assert_async().await; } + #[tokio::test] + async fn test_handle_delete_series_event() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + SonarrEvent::DeleteSeries(None), + Some("/1"), + Some("deleteFiles=true&addImportExclusion=true"), + ) + .await; + { + let mut app = app_arc.lock().await; + app.data.sonarr_data.series.set_items(vec![series()]); + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::DeleteSeries(None)) + .await + .is_ok()); + + async_server.assert_async().await; + assert!(!app_arc.lock().await.data.sonarr_data.delete_series_files); + assert!(!app_arc.lock().await.data.sonarr_data.add_list_exclusion); + } + + #[tokio::test] + async fn test_handle_delete_series_event_use_provided_params() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + SonarrEvent::DeleteSeries(None), + Some("/1"), + Some("deleteFiles=true&addImportExclusion=true"), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + let delete_series_params = DeleteSeriesParams { + id: 1, + delete_series_files: true, + add_list_exclusion: true, + }; + + assert!(network + .handle_sonarr_event(SonarrEvent::DeleteSeries(Some(delete_series_params))) + .await + .is_ok()); + + async_server.assert_async().await; + assert!(!app_arc.lock().await.data.sonarr_data.delete_series_files); + assert!(!app_arc.lock().await.data.sonarr_data.add_list_exclusion); + } + #[tokio::test] async fn test_handle_delete_sonarr_tag_event() { let (async_server, app_arc, _server) = mock_servarr_api(