From 16ca8841a1b35219f04f6630c780f96e80abe727 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 22 Nov 2024 16:46:36 -0700 Subject: [PATCH] feat(network): Support for fetching Sonarr updates --- src/models/radarr_models.rs | 20 +--- src/models/servarr_data/sonarr/sonarr_data.rs | 4 +- .../servarr_data/sonarr/sonarr_data_tests.rs | 1 + src/models/servarr_models.rs | 18 ++++ src/models/sonarr_models.rs | 16 +-- src/models/sonarr_models_tests.rs | 14 ++- src/network/radarr_network.rs | 8 +- src/network/radarr_network_tests.rs | 2 +- src/network/sonarr_network.rs | 83 ++++++++++++++- src/network/sonarr_network_tests.rs | 100 +++++++++++++++++- 10 files changed, 230 insertions(+), 36 deletions(-) diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index ecb7b16..28fa649 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -11,7 +11,7 @@ use crate::{models::HorizontallyScrollableText, serde_enum_from}; use super::servarr_models::{ DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper, - QueueEvent, Release, RootFolder, SecurityConfig, Tag, + QueueEvent, Release, RootFolder, SecurityConfig, Tag, Update, }; use super::{EnumDisplayStyle, Serdeable}; @@ -471,24 +471,6 @@ impl Display for RadarrTaskName { } } -#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct Update { - pub version: String, - pub release_date: DateTime, - pub installed: bool, - pub latest: bool, - pub installed_on: Option>, - pub changes: UpdateChanges, -} - -#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct UpdateChanges { - pub new: Option>, - pub fixed: Option>, -} - #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(untagged)] #[allow(clippy::large_enum_variant)] diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index db5461f..f497a86 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -9,7 +9,7 @@ use crate::models::{ }, stateful_list::StatefulList, stateful_table::StatefulTable, - HorizontallyScrollableText, Route, + HorizontallyScrollableText, Route, ScrollableText, }; use super::modals::SeasonDetailsModal; @@ -37,6 +37,7 @@ pub struct SonarrData { pub start_time: DateTime, pub tags_map: BiMap, pub tasks: StatefulTable, + pub updates: ScrollableText, pub version: String, } @@ -61,6 +62,7 @@ impl Default for SonarrData { start_time: DateTime::default(), tags_map: BiMap::default(), tasks: StatefulTable::default(), + updates: ScrollableText::default(), version: String::new(), } } diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index 495a5af..ca44f74 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -52,6 +52,7 @@ mod tests { assert_eq!(sonarr_data.start_time, >::default()); assert!(sonarr_data.tags_map.is_empty()); assert!(sonarr_data.tasks.is_empty()); + assert!(sonarr_data.updates.is_empty()); assert!(sonarr_data.version.is_empty()); } } diff --git a/src/models/servarr_models.rs b/src/models/servarr_models.rs index 82cdc9e..9c95dcb 100644 --- a/src/models/servarr_models.rs +++ b/src/models/servarr_models.rs @@ -249,3 +249,21 @@ pub struct UnmappedFolder { pub name: String, pub path: String, } + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Update { + pub version: String, + pub release_date: DateTime, + pub installed: bool, + pub latest: bool, + pub installed_on: Option>, + pub changes: UpdateChanges, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct UpdateChanges { + pub new: Option>, + pub fixed: Option>, +} diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index e5ad2a6..a891eaa 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -12,7 +12,7 @@ use crate::serde_enum_from; use super::{ servarr_models::{ DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper, - QueueEvent, Release, RootFolder, SecurityConfig, Tag, + QueueEvent, Release, RootFolder, SecurityConfig, Tag, Update, }, EnumDisplayStyle, HorizontallyScrollableText, Serdeable, }; @@ -430,7 +430,7 @@ impl Display for SonarrTaskName { #[serde(untagged)] #[allow(clippy::large_enum_variant)] pub enum SonarrSerdeable { - Value(Value), + BlocklistResponse(BlocklistResponse), DownloadsResponse(DownloadsResponse), DiskSpaces(Vec), Episode(Episode), @@ -438,6 +438,7 @@ pub enum SonarrSerdeable { HostConfig(HostConfig), IndexerSettings(IndexerSettings), Indexers(Vec), + LogResponse(LogResponse), QualityProfiles(Vec), QueueEvents(Vec), Releases(Vec), @@ -451,8 +452,8 @@ pub enum SonarrSerdeable { Tag(Tag), Tags(Vec), Tasks(Vec), - BlocklistResponse(BlocklistResponse), - LogResponse(LogResponse), + Updates(Vec), + Value(Value), } impl From for Serdeable { @@ -469,7 +470,7 @@ impl From<()> for SonarrSerdeable { serde_enum_from!( SonarrSerdeable { - Value(Value), + BlocklistResponse(BlocklistResponse), DownloadsResponse(DownloadsResponse), DiskSpaces(Vec), Episode(Episode), @@ -477,6 +478,7 @@ serde_enum_from!( HostConfig(HostConfig), IndexerSettings(IndexerSettings), Indexers(Vec), + LogResponse(LogResponse), QualityProfiles(Vec), QueueEvents(Vec), Releases(Vec), @@ -490,8 +492,8 @@ serde_enum_from!( Tag(Tag), Tags(Vec), Tasks(Vec), - BlocklistResponse(BlocklistResponse), - LogResponse(LogResponse), + Updates(Vec), + Value(Value), } ); diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs index abf970a..1893ac6 100644 --- a/src/models/sonarr_models_tests.rs +++ b/src/models/sonarr_models_tests.rs @@ -6,7 +6,7 @@ mod tests { use crate::models::{ servarr_models::{ DiskSpace, HostConfig, Indexer, Log, LogResponse, QualityProfile, QueueEvent, Release, - RootFolder, SecurityConfig, Tag, + RootFolder, SecurityConfig, Tag, Update, }, sonarr_models::{ BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, @@ -426,4 +426,16 @@ mod tests { assert_eq!(sonarr_serdeable, SonarrSerdeable::Tasks(tasks)); } + + #[test] + fn test_sonarr_serdeable_from_updates() { + let updates = vec![Update { + version: "test".to_owned(), + ..Update::default() + }]; + + let sonarr_serdeable: SonarrSerdeable = updates.clone().into(); + + assert_eq!(sonarr_serdeable, SonarrSerdeable::Updates(updates)); + } } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 88bf9ea..1bf6b7e 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -11,7 +11,7 @@ use crate::models::radarr_models::{ CommandBody, Credit, CreditType, DeleteMovieParams, DownloadRecord, DownloadsResponse, EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings, IndexerTestResult, Movie, MovieCommandBody, MovieHistoryItem, RadarrSerdeable, RadarrTask, RadarrTaskName, - ReleaseDownloadBody, SystemStatus, Update, + ReleaseDownloadBody, SystemStatus, }; use crate::models::servarr_data::radarr::modals::{ AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem, @@ -20,7 +20,7 @@ use crate::models::servarr_data::radarr::modals::{ use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ AddRootFolderBody, DiskSpace, HostConfig, Indexer, LogResponse, QualityProfile, QueueEvent, - Release, RootFolder, SecurityConfig, Tag, + Release, RootFolder, SecurityConfig, Tag, Update, }; use crate::models::stateful_table::StatefulTable; use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText}; @@ -244,7 +244,7 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::GetStatus => self.get_radarr_status().await.map(RadarrSerdeable::from), RadarrEvent::GetTags => self.get_radarr_tags().await.map(RadarrSerdeable::from), RadarrEvent::GetTasks => self.get_radarr_tasks().await.map(RadarrSerdeable::from), - RadarrEvent::GetUpdates => self.get_updates().await.map(RadarrSerdeable::from), + RadarrEvent::GetUpdates => self.get_radarr_updates().await.map(RadarrSerdeable::from), RadarrEvent::HealthCheck => self .get_radarr_healthcheck() .await @@ -1870,7 +1870,7 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_updates(&mut self) -> Result> { + async fn get_radarr_updates(&mut self) -> Result> { info!("Fetching Radarr updates"); let event = RadarrEvent::GetUpdates; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index c4cc9f6..1f0dc5f 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -2730,7 +2730,7 @@ mod test { } #[tokio::test] - async fn test_handle_get_updates_event() { + async fn test_handle_get_radarr_updates_event() { let updates_json = json!([{ "version": "4.3.2.1", "releaseDate": "2023-04-15T02:02:53Z", diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index f8029ff..adb9f8e 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -11,7 +11,7 @@ use crate::{ }, servarr_models::{ AddRootFolderBody, DiskSpace, HostConfig, Indexer, LogResponse, QualityProfile, QueueEvent, - Release, RootFolder, SecurityConfig, Tag, + Release, RootFolder, SecurityConfig, Tag, Update, }, sonarr_models::{ BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, IndexerSettings, Series, @@ -59,6 +59,7 @@ pub enum SonarrEvent { GetSeriesDetails(Option), GetSeriesHistory(Option), GetStatus, + GetUpdates, GetTags, GetTasks, HealthCheck, @@ -90,6 +91,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetSeriesHistory(_) => "/history/series", SonarrEvent::GetStatus => "/system/status", SonarrEvent::GetTasks => "/system/task", + SonarrEvent::GetUpdates => "/update", SonarrEvent::HealthCheck => "/health", SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) => "/series", SonarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", @@ -205,6 +207,7 @@ impl<'a, 'b> Network<'a, 'b> { SonarrEvent::GetStatus => self.get_sonarr_status().await.map(SonarrSerdeable::from), SonarrEvent::GetTags => self.get_sonarr_tags().await.map(SonarrSerdeable::from), SonarrEvent::GetTasks => self.get_sonarr_tasks().await.map(SonarrSerdeable::from), + SonarrEvent::GetUpdates => self.get_sonarr_updates().await.map(SonarrSerdeable::from), SonarrEvent::HealthCheck => self .get_sonarr_healthcheck() .await @@ -1174,6 +1177,84 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_sonarr_updates(&mut self) -> Result> { + info!("Fetching Sonarr updates"); + let event = SonarrEvent::GetUpdates; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |updates_vec, mut app| { + let latest_installed = if updates_vec + .iter() + .any(|update| update.latest && update.installed_on.is_some()) + { + "already".to_owned() + } else { + "not".to_owned() + }; + let updates = updates_vec + .into_iter() + .map(|update| { + let install_status = if update.installed_on.is_some() { + if update.installed { + "(Currently Installed)".to_owned() + } else { + "(Previously Installed)".to_owned() + } + } else { + String::new() + }; + let vec_to_bullet_points = |vec: Vec| { + vec + .iter() + .map(|change| format!(" * {change}")) + .collect::>() + .join("\n") + }; + + let mut update_info = formatdoc!( + "{} - {} {install_status} + {}", + update.version, + update.release_date, + "-".repeat(200) + ); + + if let Some(new_changes) = update.changes.new { + let changes = vec_to_bullet_points(new_changes); + update_info = formatdoc!( + "{update_info} + New: + {changes}" + ) + } + + if let Some(fixes) = update.changes.fixed { + let fixes = vec_to_bullet_points(fixes); + update_info = formatdoc!( + "{update_info} + Fixed: + {fixes}" + ); + } + + update_info + }) + .reduce(|version_1, version_2| format!("{version_1}\n\n\n{version_2}")) + .unwrap(); + + app.data.sonarr_data.updates = ScrollableText::with_string(formatdoc!( + "The latest version of Sonarr is {latest_installed} installed + + {updates}" + )); + }) + .await + } + async fn mark_sonarr_history_item_as_failed(&mut self, history_item_id: i64) -> Result { info!("Marking the Sonarr history item with ID: {history_item_id} as 'failed'"); let event = SonarrEvent::MarkHistoryItemAsFailed(history_item_id); diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 9eddb75..9834011 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -18,7 +18,7 @@ mod test { use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_models::{ DiskSpace, HostConfig, Indexer, IndexerField, Language, LogResponse, Quality, QualityProfile, - QualityWrapper, QueueEvent, Release, RootFolder, SecurityConfig, Tag, + QualityWrapper, QueueEvent, Release, RootFolder, SecurityConfig, Tag, Update, }; use crate::models::sonarr_models::{ BlocklistItem, DownloadRecord, DownloadsResponse, Episode, EpisodeFile, MediaInfo, @@ -29,8 +29,8 @@ mod test { }; use crate::models::sonarr_models::{SonarrTask, SystemStatus}; use crate::models::stateful_table::StatefulTable; - use crate::models::HorizontallyScrollableText; use crate::models::{sonarr_models::SonarrSerdeable, stateful_table::SortOption}; + use crate::models::{HorizontallyScrollableText, ScrollableText}; use crate::network::sonarr_network::get_episode_status; use crate::{ @@ -219,6 +219,7 @@ mod test { #[case(SonarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(SonarrEvent::GetStatus, "/system/status")] #[case(SonarrEvent::GetTasks, "/system/task")] + #[case(SonarrEvent::GetUpdates, "/update")] #[case(SonarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")] fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) { assert_str_eq!(event.resource(), expected_uri); @@ -3741,6 +3742,101 @@ mod test { } } + #[tokio::test] + async fn test_handle_get_sonarr_updates_event() { + let updates_json = json!([{ + "version": "4.3.2.1", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": true, + "installedOn": "2023-04-15T02:02:53Z", + "latest": true, + "changes": { + "new": [ + "Cool new thing" + ], + "fixed": [ + "Some bugs killed" + ] + }, + }, + { + "version": "3.2.1.0", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": false, + "installedOn": "2023-04-15T02:02:53Z", + "latest": false, + "changes": { + "new": [ + "Cool new thing (old)", + "Other cool new thing (old)" + ], + }, + }, + { + "version": "2.1.0", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": false, + "latest": false, + "changes": { + "fixed": [ + "Killed bug 1", + "Fixed bug 2" + ] + }, + }]); + let response: Vec = serde_json::from_value(updates_json.clone()).unwrap(); + let line_break = "-".repeat(200); + let expected_text = ScrollableText::with_string(formatdoc!( + " + The latest version of Sonarr is already installed + + 4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed) + {line_break} + New: + * Cool new thing + Fixed: + * Some bugs killed + + + 3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed) + {line_break} + New: + * Cool new thing (old) + * Other cool new thing (old) + + + 2.1.0 - 2023-04-15 02:02:53 UTC + {line_break} + Fixed: + * Killed bug 1 + * Fixed bug 2" + )); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(updates_json), + None, + SonarrEvent::GetUpdates, + None, + None, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::Updates(updates) = network + .handle_sonarr_event(SonarrEvent::GetUpdates) + .await + .unwrap() + { + async_server.assert_async().await; + assert_str_eq!( + app_arc.lock().await.data.sonarr_data.updates.get_text(), + expected_text.get_text() + ); + assert_eq!(updates, response); + } + } + #[tokio::test] async fn test_handle_mark_sonarr_history_item_as_failed_event() { let expected_history_item_id = 1;