From d8979221c80c1cc4e3ee3fd69004ed75f9aee93f Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 18 Nov 2024 19:54:42 -0700 Subject: [PATCH] feat(network): Added the GetIndexers network call for Sonarr --- src/models/servarr_data/sonarr/sonarr_data.rs | 4 +- .../servarr_data/sonarr/sonarr_data_tests.rs | 1 + src/models/sonarr_models.rs | 31 ++++++ src/network/radarr_network.rs | 4 +- src/network/radarr_network_tests.rs | 2 +- src/network/sonarr_network.rs | 22 +++- src/network/sonarr_network_tests.rs | 101 +++++++++++++++++- 7 files changed, 157 insertions(+), 8 deletions(-) diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 3cb44b6..39f3b13 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use strum::EnumIter; use crate::models::{ - sonarr_models::{BlocklistItem, DownloadRecord, Episode, Series}, + sonarr_models::{BlocklistItem, DownloadRecord, Episode, Indexer, Series}, stateful_list::StatefulList, stateful_table::StatefulTable, stateful_tree::StatefulTree, @@ -27,6 +27,7 @@ pub struct SonarrData { pub downloads: StatefulTable, pub episode_details_modal: Option, pub quality_profile_map: BiMap, + pub indexers: StatefulTable, } impl Default for SonarrData { @@ -42,6 +43,7 @@ impl Default for SonarrData { downloads: StatefulTable::default(), episode_details_modal: None, quality_profile_map: BiMap::new(), + indexers: StatefulTable::default(), } } } diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index 24d40ea..2f44e42 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -44,6 +44,7 @@ mod tests { assert!(sonarr_data.downloads.is_empty()); assert!(sonarr_data.episode_details_modal.is_none()); assert!(sonarr_data.quality_profile_map.is_empty()); + assert!(sonarr_data.indexers.is_empty()); } } } diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 6e1d3b1..6b5f6d9 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -103,6 +103,35 @@ pub struct EpisodeFile { pub media_info: Option, } +#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Indexer { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub name: Option, + pub implementation: Option, + pub implementation_name: Option, + pub config_contract: Option, + pub supports_rss: bool, + pub supports_search: bool, + pub fields: Option>, + pub enable_rss: bool, + pub enable_automatic_search: bool, + pub enable_interactive_search: bool, + pub protocol: String, + #[serde(deserialize_with = "super::from_i64")] + pub priority: i64, + #[serde(deserialize_with = "super::from_i64")] + pub download_client_id: i64, + pub tags: Vec, +} + +#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +pub struct IndexerField { + pub name: Option, + pub value: Option, +} + #[derive(Serialize, Deserialize, Default, Debug, Hash, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct Language { pub name: String, @@ -330,6 +359,7 @@ pub enum SonarrSerdeable { DownloadsResponse(DownloadsResponse), Episode(Episode), Episodes(Vec), + Indexers(Vec), QualityProfiles(Vec), SeriesVec(Vec), SystemStatus(SystemStatus), @@ -355,6 +385,7 @@ serde_enum_from!( DownloadsResponse(DownloadsResponse), Episode(Episode), Episodes(Vec), + Indexers(Vec), QualityProfiles(Vec), SeriesVec(Vec), SystemStatus(SystemStatus), diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index d673623..8924dd5 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -193,7 +193,7 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::GetCollections => self.get_collections().await.map(RadarrSerdeable::from), RadarrEvent::GetDownloads => self.get_radarr_downloads().await.map(RadarrSerdeable::from), RadarrEvent::GetHostConfig => self.get_host_config().await.map(RadarrSerdeable::from), - RadarrEvent::GetIndexers => self.get_indexers().await.map(RadarrSerdeable::from), + RadarrEvent::GetIndexers => self.get_radarr_indexers().await.map(RadarrSerdeable::from), RadarrEvent::GetLogs(events) => self .get_radarr_logs(events) .await @@ -1395,7 +1395,7 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_indexers(&mut self) -> Result> { + async fn get_radarr_indexers(&mut self) -> Result> { info!("Fetching Radarr indexers"); let event = RadarrEvent::GetIndexers; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 2957295..bd98025 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -2261,7 +2261,7 @@ mod test { } #[tokio::test] - async fn test_handle_get_indexers_event() { + async fn test_handle_get_radarr_indexers_event() { let indexers_response_json = json!([{ "enableRss": true, "enableAutomaticSearch": true, diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 29423df..2db80ff 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -10,8 +10,8 @@ use crate::{ models::{ servarr_data::sonarr::{modals::EpisodeDetailsModal, sonarr_data::ActiveSonarrBlock}, sonarr_models::{ - BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, LogResponse, QualityProfile, - Series, SonarrSerdeable, SystemStatus, + BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, Indexer, LogResponse, + QualityProfile, Series, SonarrSerdeable, SystemStatus, }, HorizontallyScrollableText, Route, Scrollable, ScrollableText, }, @@ -30,6 +30,7 @@ pub enum SonarrEvent { DeleteBlocklistItem(Option), GetBlocklist, GetDownloads, + GetIndexers, GetEpisodeDetails(Option), GetEpisodes(Option), GetLogs(Option), @@ -47,6 +48,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", SonarrEvent::GetDownloads => "/queue", SonarrEvent::GetEpisodes(_) | SonarrEvent::GetEpisodeDetails(_) => "/episode", + SonarrEvent::GetIndexers => "/indexer", SonarrEvent::GetLogs(_) => "/log", SonarrEvent::GetQualityProfiles => "/qualityprofile", SonarrEvent::GetStatus => "/system/status", @@ -86,6 +88,7 @@ impl<'a, 'b> Network<'a, 'b> { .get_episode_details(episode_id) .await .map(SonarrSerdeable::from), + SonarrEvent::GetIndexers => self.get_sonarr_indexers().await.map(SonarrSerdeable::from), SonarrEvent::GetQualityProfiles => self .get_sonarr_quality_profiles() .await @@ -390,6 +393,21 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_sonarr_indexers(&mut self) -> Result> { + info!("Fetching Sonarr indexers"); + let event = SonarrEvent::GetIndexers; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |indexers, mut app| { + app.data.sonarr_data.indexers.set_items(indexers); + }) + .await + } + async fn get_sonarr_logs(&mut self, events: Option) -> Result { info!("Fetching Sonarr logs"); let event = SonarrEvent::GetLogs(events); diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 1f5d72a..8d3b31e 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -23,8 +23,8 @@ mod test { use crate::app::App; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::sonarr_models::{ - BlocklistItem, DownloadRecord, DownloadsResponse, Episode, EpisodeFile, Language, LogResponse, - MediaInfo, QualityProfile, + BlocklistItem, DownloadRecord, DownloadsResponse, Episode, EpisodeFile, Indexer, IndexerField, + Language, LogResponse, MediaInfo, QualityProfile, }; use crate::models::sonarr_models::{BlocklistResponse, Quality}; use crate::models::sonarr_models::{QualityWrapper, SystemStatus}; @@ -137,6 +137,11 @@ mod test { assert_str_eq!(event.resource(), "/series"); } + #[rstest] + fn test_resource_indexer(#[values(SonarrEvent::GetIndexers)] event: SonarrEvent) { + assert_str_eq!(event.resource(), "/indexer"); + } + #[rstest] #[case(SonarrEvent::ClearBlocklist, "/blocklist/bulk")] #[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")] @@ -646,6 +651,65 @@ mod test { } } + #[tokio::test] + async fn test_handle_get_sonarr_indexers_event() { + let indexers_response_json = json!([{ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "supportsRss": true, + "supportsSearch": true, + "protocol": "torrent", + "priority": 25, + "downloadClientId": 0, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "implementationName": "Torznab", + "implementation": "Torznab", + "configContract": "TorznabSettings", + "tags": [1], + "id": 1 + }]); + let response: Vec = serde_json::from_value(indexers_response_json.clone()).unwrap(); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(indexers_response_json), + None, + SonarrEvent::GetIndexers, + None, + None, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::Indexers(indexers) = network + .handle_sonarr_event(SonarrEvent::GetIndexers) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.sonarr_data.indexers.items, + vec![indexer()] + ); + assert_eq!(indexers, response); + } + } + #[tokio::test] async fn test_handle_get_episodes_event_uses_provided_series_id() { let episodes_json = json!([ @@ -1499,6 +1563,39 @@ mod test { } } + fn indexer() -> Indexer { + Indexer { + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + supports_rss: true, + supports_search: true, + protocol: "torrent".to_owned(), + priority: 25, + download_client_id: 0, + name: Some("Test Indexer".to_owned()), + implementation_name: Some("Torznab".to_owned()), + implementation: Some("Torznab".to_owned()), + config_contract: Some("TorznabSettings".to_owned()), + tags: vec![Number::from(1)], + id: 1, + fields: Some(vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(json!("https://test.com")), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(json!("")), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(json!("1.2")), + }, + ]), + } + } + fn language() -> Language { Language { name: "English".to_owned(),