From 2b9ddd0d1efe46543d7ce46897ace7e910e42fb8 Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:07 -0600 Subject: [PATCH] Added network support for updating all indexer settings, editing specific indexer settings, deleting an indexer; Also added keybindings for all of the above that change the current route. Added full support for deleting an indexer; still need to add an indexer_handler to handle the add, edit, and settings functionalities --- src/app/key_binding.rs | 5 + src/app/radarr.rs | 26 ++++- src/app/radarr_tests.rs | 20 +++- src/handlers/radarr_handlers/mod.rs | 27 +++++ .../radarr_handler_test_utils.rs | 17 +++ .../radarr_handlers/radarr_handler_tests.rs | 106 +++++++++++------ src/models/radarr_models.rs | 22 +++- src/network/radarr_network.rs | 95 +++++++++++++++- src/network/radarr_network_tests.rs | 107 +++++++++++++++++- src/ui/radarr_ui/indexers_ui.rs | 44 ++++++- src/ui/radarr_ui/mod.rs | 8 +- 11 files changed, 419 insertions(+), 58 deletions(-) diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index b8e154c..e195677 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -16,6 +16,7 @@ generate_keybindings! { right, backspace, search, + settings, filter, sort, edit, @@ -66,6 +67,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::Char('s'), desc: "Search", }, + settings: KeyBinding { + key: Key::Char('s'), + desc: "Settings", + }, filter: KeyBinding { key: Key::Char('f'), desc: "Filter", diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 319afb6..dd0ba1c 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -5,8 +5,8 @@ use strum::IntoEnumIterator; use crate::app::{App, Route}; use crate::models::radarr_models::{ AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Indexer, - MinimumAvailability, Monitor, Movie, MovieHistoryItem, QueueEvent, Release, ReleaseField, - RootFolder, Task, + IndexerSettings, MinimumAvailability, Monitor, Movie, MovieHistoryItem, QueueEvent, Release, + ReleaseField, RootFolder, Task, }; use crate::models::{ BlockSelectionState, HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable, @@ -37,6 +37,7 @@ pub struct RadarrData<'a> { pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>, pub downloads: StatefulTable, pub indexers: StatefulTable, + pub indexer_settings: Option, pub quality_profile_map: BiMap, pub tags_map: BiMap, pub movie_details: ScrollableText, @@ -263,6 +264,7 @@ impl<'a> Default for RadarrData<'a> { filtered_movies: StatefulTable::default(), downloads: StatefulTable::default(), indexers: StatefulTable::default(), + indexer_settings: None, quality_profile_map: BiMap::default(), tags_map: BiMap::default(), file_details: String::default(), @@ -324,7 +326,7 @@ impl<'a> Default for RadarrData<'a> { title: "Indexers", route: ActiveRadarrBlock::Indexers.into(), help: "", - contextual_help: Some(" refresh"), + contextual_help: Some(" edit | settings | delete | refresh"), }, TabRoute { title: "System", @@ -377,6 +379,7 @@ impl<'a> Default for RadarrData<'a> { #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub enum ActiveRadarrBlock { + AddIndexer, AddMovieAlreadyInLibrary, AddMovieSearchInput, AddMovieSearchResults, @@ -394,11 +397,12 @@ pub enum ActiveRadarrBlock { CollectionDetails, Cast, Crew, + DeleteDownloadPrompt, + DeleteIndexerPrompt, DeleteMoviePrompt, DeleteMovieConfirmPrompt, DeleteMovieToggleDeleteFile, DeleteMovieToggleAddListExclusion, - DeleteDownloadPrompt, DeleteRootFolderPrompt, Downloads, EditCollectionPrompt, @@ -408,6 +412,7 @@ pub enum ActiveRadarrBlock { EditCollectionSelectQualityProfile, EditCollectionToggleSearchOnAdd, EditCollectionToggleMonitored, + EditIndexer, EditMoviePrompt, EditMovieConfirmPrompt, EditMoviePathInput, @@ -419,6 +424,7 @@ pub enum ActiveRadarrBlock { FilterCollections, FilterMovies, Indexers, + IndexerSettings, ManualSearch, ManualSearchSortPrompt, ManualSearchConfirmPrompt, @@ -531,6 +537,13 @@ pub static DELETE_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 3] = [ ActiveRadarrBlock::DeleteMovieToggleAddListExclusion, ActiveRadarrBlock::DeleteMovieConfirmPrompt, ]; +pub static INDEXER_BLOCKS: [ActiveRadarrBlock; 5] = [ + ActiveRadarrBlock::Indexers, + ActiveRadarrBlock::IndexerSettings, + ActiveRadarrBlock::AddIndexer, + ActiveRadarrBlock::EditIndexer, + ActiveRadarrBlock::DeleteIndexerPrompt, +]; pub static SYSTEM_DETAILS_BLOCKS: [ActiveRadarrBlock; 5] = [ ActiveRadarrBlock::SystemLogs, ActiveRadarrBlock::SystemQueuedEvents, @@ -587,6 +600,11 @@ impl<'a> App<'a> { .dispatch_network_event(RadarrEvent::GetIndexers.into()) .await; } + ActiveRadarrBlock::IndexerSettings => { + self + .dispatch_network_event(RadarrEvent::GetIndexerSettings.into()) + .await; + } ActiveRadarrBlock::System => { self .dispatch_network_event(RadarrEvent::GetTasks.into()) diff --git a/src/app/radarr_tests.rs b/src/app/radarr_tests.rs index 246b608..1312640 100644 --- a/src/app/radarr_tests.rs +++ b/src/app/radarr_tests.rs @@ -274,6 +274,7 @@ mod tests { assert!(radarr_data.filtered_movies.items.is_empty()); assert!(radarr_data.downloads.items.is_empty()); assert!(radarr_data.indexers.items.is_empty()); + assert!(radarr_data.indexer_settings.is_none()); assert!(radarr_data.quality_profile_map.is_empty()); assert!(radarr_data.tags_map.is_empty()); assert!(radarr_data.file_details.is_empty()); @@ -357,7 +358,7 @@ mod tests { assert!(radarr_data.main_tabs.tabs[4].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[4].contextual_help, - Some(" refresh") + Some(" edit | settings | delete | refresh") ); assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System"); @@ -713,6 +714,23 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_indexer_settings_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::IndexerSettings) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetIndexerSettings.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_dispatch_by_system_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 6e0001c..9a92c19 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -251,6 +251,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b ActiveRadarrBlock::RootFolders => self .app .push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into()), + ActiveRadarrBlock::Indexers => self + .app + .push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()), _ => (), } } @@ -278,6 +281,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b _ => (), }, ActiveRadarrBlock::DeleteDownloadPrompt + | ActiveRadarrBlock::DeleteIndexerPrompt | ActiveRadarrBlock::DeleteRootFolderPrompt | ActiveRadarrBlock::UpdateAllMoviesPrompt | ActiveRadarrBlock::UpdateAllCollectionsPrompt @@ -404,6 +408,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b self.app.pop_navigation_stack(); } + ActiveRadarrBlock::DeleteIndexerPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer); + } + + self.app.pop_navigation_stack(); + } ActiveRadarrBlock::UpdateAllMoviesPrompt => { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies); @@ -431,6 +442,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b self.app.should_ignore_quit_key = false; self.app.pop_navigation_stack(); } + ActiveRadarrBlock::Indexers => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::EditIndexer.into()); + } _ => (), } } @@ -454,6 +470,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b self.app.should_ignore_quit_key = false; } ActiveRadarrBlock::DeleteDownloadPrompt + | ActiveRadarrBlock::DeleteIndexerPrompt | ActiveRadarrBlock::DeleteRootFolderPrompt | ActiveRadarrBlock::UpdateAllMoviesPrompt | ActiveRadarrBlock::UpdateAllCollectionsPrompt @@ -527,9 +544,19 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b _ => (), }, ActiveRadarrBlock::Indexers => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.add.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::AddIndexer.into()); + } _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } + _ if *key == DEFAULT_KEYBINDINGS.settings.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::IndexerSettings.into()); + } _ => (), }, ActiveRadarrBlock::Collections => match self.key { diff --git a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs index ab3fb45..43d890e 100644 --- a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs +++ b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs @@ -135,4 +135,21 @@ mod utils { assert_eq!(app.data.radarr_data.edit_search_on_add, Some(true)); }; } + + #[macro_export] + macro_rules! assert_delete_prompt { + ($block:expr, $expected_block:expr) => { + let mut app = App::default(); + + RadarrHandler::with(&DELETE_KEY, &mut app, &$block, &None).handle(); + + assert_eq!(app.get_current_route(), &$expected_block.into()); + }; + + ($app:expr, $block:expr, $expected_block:expr) => { + RadarrHandler::with(&DELETE_KEY, &mut $app, &$block, &None).handle(); + + assert_eq!($app.get_current_route(), &$expected_block.into()); + }; + } } diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index 8589942..5a39255 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -209,6 +209,8 @@ mod tests { mod test_handle_delete { use pretty_assertions::assert_eq; + use crate::assert_delete_prompt; + use super::*; const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; @@ -217,11 +219,10 @@ mod tests { fn test_movies_delete() { let mut app = App::default(); - RadarrHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::DeleteMoviePrompt.into() + assert_delete_prompt!( + app, + ActiveRadarrBlock::Movies, + ActiveRadarrBlock::DeleteMoviePrompt ); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), @@ -229,34 +230,18 @@ mod tests { ); } - #[test] - fn test_downloads_delete() { - let mut app = App::default(); - - RadarrHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::DeleteDownloadPrompt.into() - ); - } - - #[test] - fn test_root_folder_delete() { - let mut app = App::default(); - - RadarrHandler::with( - &DELETE_KEY, - &mut app, - &ActiveRadarrBlock::RootFolders, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::DeleteRootFolderPrompt.into() - ); + #[rstest] + #[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::DeleteDownloadPrompt)] + #[case( + ActiveRadarrBlock::RootFolders, + ActiveRadarrBlock::DeleteRootFolderPrompt + )] + #[case(ActiveRadarrBlock::Indexers, ActiveRadarrBlock::DeleteIndexerPrompt)] + fn test_delete_prompt( + #[case] active_radarr_block: ActiveRadarrBlock, + #[case] expected_radarr_block: ActiveRadarrBlock, + ) { + assert_delete_prompt!(active_radarr_block, expected_radarr_block); } } @@ -332,6 +317,7 @@ mod tests { fn test_left_right_prompt_toggle( #[values( ActiveRadarrBlock::DeleteDownloadPrompt, + ActiveRadarrBlock::DeleteIndexerPrompt, ActiveRadarrBlock::DeleteRootFolderPrompt, ActiveRadarrBlock::UpdateAllMoviesPrompt, ActiveRadarrBlock::UpdateAllCollectionsPrompt, @@ -387,6 +373,18 @@ mod tests { const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + #[test] + fn test_indexer_submit_aka_edit() { + let mut app = App::default(); + + RadarrHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexer.into() + ); + } + #[rstest] #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::MovieDetails)] #[case(ActiveRadarrBlock::Collections, ActiveRadarrBlock::CollectionDetails)] @@ -629,6 +627,11 @@ mod tests { ActiveRadarrBlock::DeleteDownloadPrompt, RadarrEvent::DeleteDownload )] + #[case( + ActiveRadarrBlock::Indexers, + ActiveRadarrBlock::DeleteIndexerPrompt, + RadarrEvent::DeleteIndexer + )] #[case( ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::DeleteRootFolderPrompt, @@ -751,6 +754,7 @@ mod tests { ActiveRadarrBlock::DeleteRootFolderPrompt )] #[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::DeleteDownloadPrompt)] + #[case(ActiveRadarrBlock::Indexers, ActiveRadarrBlock::DeleteIndexerPrompt)] #[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::UpdateDownloadsPrompt)] #[case( ActiveRadarrBlock::Collections, @@ -899,6 +903,24 @@ mod tests { assert!(app.should_ignore_quit_key); } + #[test] + fn test_indexer_add() { + let mut app = App::default(); + + RadarrHandler::with( + &DEFAULT_KEYBINDINGS.add.key, + &mut app, + &ActiveRadarrBlock::Indexers, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::AddIndexer.into() + ); + } + #[test] fn test_root_folder_add() { let mut app = App::default(); @@ -1054,6 +1076,24 @@ mod tests { ); } + #[test] + fn test_indexer_settings_key() { + let mut app = App::default(); + + RadarrHandler::with( + &DEFAULT_KEYBINDINGS.settings.key, + &mut app, + &ActiveRadarrBlock::Indexers, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::IndexerSettings.into() + ); + } + #[test] fn test_add_root_folder_prompt_backspace_key() { let mut app = App::default(); diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 87a7d2d..cd93cca 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -182,7 +182,6 @@ pub struct IndexerField { pub value: Option, #[serde(rename(deserialize = "type"))] pub field_type: Option, - pub advanced: bool, pub select_options: Option>, } @@ -197,6 +196,27 @@ pub struct IndexerSelectOption { pub order: Number, } +#[derive(Derivative, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct IndexerSettings { + pub allow_hardcoded_subs: bool, + #[derivative(Default(value = "Number::from(0)"))] + pub availability_delay: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub maximum_size: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub minimum_age: Number, + pub prefer_indexer_flags: bool, + #[derivative(Default(value = "Number::from(0)"))] + pub retention: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub rss_sync_interval: Number, + pub whitelisted_hardcoded_subs: String, +} + #[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct Language { pub name: String, diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 0da38f0..9245558 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -11,8 +11,8 @@ use crate::app::RadarrConfig; use crate::models::radarr_models::{ AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie, CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Indexer, - LogResponse, Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, QueueEvent, Release, - ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, Update, + IndexerSettings, LogResponse, Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, + QueueEvent, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, Update, }; use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText}; use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps}; @@ -27,6 +27,7 @@ pub enum RadarrEvent { AddMovie, AddRootFolder, DeleteDownload, + DeleteIndexer, DeleteMovie, DeleteRootFolder, DownloadRelease, @@ -35,6 +36,7 @@ pub enum RadarrEvent { GetCollections, GetDownloads, GetIndexers, + GetIndexerSettings, GetLogs, GetMovieCredits, GetMovieDetails, @@ -57,6 +59,7 @@ pub enum RadarrEvent { UpdateAndScan, UpdateCollections, UpdateDownloads, + UpdateIndexerSettings, } impl RadarrEvent { @@ -64,7 +67,8 @@ impl RadarrEvent { match self { RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", - RadarrEvent::GetIndexers => "/indexer", + RadarrEvent::GetIndexers | RadarrEvent::DeleteIndexer => "/indexer", + RadarrEvent::GetIndexerSettings | RadarrEvent::UpdateIndexerSettings => "/config/indexer", RadarrEvent::GetLogs => "/log", RadarrEvent::AddMovie | RadarrEvent::EditMovie @@ -107,8 +111,9 @@ impl<'a, 'b> Network<'a, 'b> { match radarr_event { RadarrEvent::AddMovie => self.add_movie().await, RadarrEvent::AddRootFolder => self.add_root_folder().await, - RadarrEvent::DeleteMovie => self.delete_movie().await, RadarrEvent::DeleteDownload => self.delete_download().await, + RadarrEvent::DeleteIndexer => self.delete_indexer().await, + RadarrEvent::DeleteMovie => self.delete_movie().await, RadarrEvent::DeleteRootFolder => self.delete_root_folder().await, RadarrEvent::DownloadRelease => self.download_release().await, RadarrEvent::EditCollection => self.edit_collection().await, @@ -116,6 +121,7 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetDownloads => self.get_downloads().await, RadarrEvent::GetIndexers => self.get_indexers().await, + RadarrEvent::GetIndexerSettings => self.get_indexer_settings().await, RadarrEvent::GetLogs => self.get_logs().await, RadarrEvent::GetMovieCredits => self.get_credits().await, RadarrEvent::GetMovieDetails => self.get_movie_details().await, @@ -138,6 +144,7 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::UpdateAndScan => self.update_and_scan().await, RadarrEvent::UpdateCollections => self.update_collections().await, RadarrEvent::UpdateDownloads => self.update_downloads().await, + RadarrEvent::UpdateIndexerSettings => self.update_indexer_settings().await, } } @@ -293,6 +300,37 @@ impl<'a, 'b> Network<'a, 'b> { .await; } + async fn delete_indexer(&self) { + let indexer_id = self + .app + .lock() + .await + .data + .radarr_data + .indexers + .current_selection() + .id + .as_u64() + .unwrap(); + + info!( + "Deleting Radarr indexer for indexer with id: {}", + indexer_id + ); + + let request_props = self + .radarr_request_props_from( + format!("{}/{}", RadarrEvent::DeleteIndexer.resource(), indexer_id).as_str(), + RequestMethod::Delete, + None::<()>, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await; + } + async fn delete_movie(&self) { let movie_id = self.extract_movie_id().await; let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; @@ -644,6 +682,24 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_indexer_settings(&self) { + info!("Fetching Radarr indexer settings"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::GetIndexerSettings.resource(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), IndexerSettings>(request_props, |indexer_settings, mut app| { + app.data.radarr_data.indexer_settings = Some(indexer_settings); + }) + .await; + } + async fn get_healthcheck(&self) { info!("Performing Radarr health check"); @@ -1302,6 +1358,37 @@ impl<'a, 'b> Network<'a, 'b> { .await; } + async fn update_indexer_settings(&self) { + info!("Updating Radarr indexer settings"); + + let body = self + .app + .lock() + .await + .data + .radarr_data + .indexer_settings + .as_ref() + .unwrap() + .clone(); + + debug!("Indexer settings body: {:?}", body); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::UpdateIndexerSettings.resource(), + RequestMethod::Put, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| {}) + .await; + + self.app.lock().await.data.radarr_data.indexer_settings = None; + } + async fn radarr_request_props_from( &self, resource: &str, diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index b152e5b..6c8dd42 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -531,6 +531,44 @@ mod test { async_server.assert_async().await; } + #[tokio::test] + async fn test_handle_update_indexer_settings_event() { + let indexer_settings_json = json!({ + "minimumAge": 0, + "maximumSize": 0, + "retention": 0, + "rssSyncInterval": 60, + "preferIndexerFlags": false, + "availabilityDelay": 0, + "allowHardcodedSubs": true, + "whitelistedHardcodedSubs": "", + "id": 1 + }); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Put, + Some(indexer_settings_json), + None, + RadarrEvent::UpdateIndexerSettings.resource(), + ) + .await; + + app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings()); + let network = Network::new(reqwest::Client::new(), &app_arc); + + network + .handle_radarr_event(RadarrEvent::UpdateIndexerSettings) + .await; + + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .radarr_data + .indexer_settings + .is_none()); + } + #[tokio::test] async fn test_handle_update_collections_event() { let (async_server, app_arc, _server) = mock_radarr_api( @@ -873,13 +911,11 @@ mod test { "label": "Value Is String", "value": "hello", "type": "textbox", - "advanced": false }, { "order": 1, "name": "emptyValueWithSelectOptions", "label": "Empty Value With Select Options", - "advanced": true, "type": "select", "selectOptions": [ { @@ -895,7 +931,6 @@ mod test { "label": "Value is an array", "value": [1, 2], "type": "select", - "advanced": false, }, ], "implementationName": "Torznab", @@ -922,6 +957,39 @@ mod test { ); } + #[tokio::test] + async fn test_handle_get_indexer_settings_event() { + let indexer_settings_response_json = json!({ + "minimumAge": 0, + "maximumSize": 0, + "retention": 0, + "rssSyncInterval": 60, + "preferIndexerFlags": false, + "availabilityDelay": 0, + "allowHardcodedSubs": true, + "whitelistedHardcodedSubs": "", + "id": 1 + }); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexer_settings_response_json), + RadarrEvent::GetIndexerSettings.resource(), + ) + .await; + let network = Network::new(reqwest::Client::new(), &app_arc); + + network + .handle_radarr_event(RadarrEvent::GetIndexerSettings) + .await; + + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.indexer_settings, + Some(indexer_settings()) + ); + } + #[tokio::test] async fn test_handle_get_queued_events_event() { let queued_events_json = json!([{ @@ -1362,6 +1430,27 @@ mod test { async_server.assert_async().await; } + #[tokio::test] + async fn test_handle_delete_indexer_event() { + let resource = format!("{}/1", RadarrEvent::DeleteIndexer.resource()); + let (async_server, app_arc, _server) = + mock_radarr_api(RequestMethod::Delete, None, None, &resource).await; + app_arc + .lock() + .await + .data + .radarr_data + .indexers + .set_items(vec![indexer()]); + let network = Network::new(reqwest::Client::new(), &app_arc); + + network + .handle_radarr_event(RadarrEvent::DeleteIndexer) + .await; + + async_server.assert_async().await; + } + #[tokio::test] async fn test_handle_delete_root_folder_event() { let resource = format!("{}/1", RadarrEvent::DeleteRootFolder.resource()); @@ -2216,7 +2305,6 @@ mod test { name: Some("valueIsString".to_owned()), label: Some("Value Is String".to_owned()), value: Some(json!("hello")), - advanced: false, field_type: Some("textbox".to_owned()), select_options: None, }, @@ -2225,7 +2313,6 @@ mod test { name: Some("emptyValueWithSelectOptions".to_owned()), label: Some("Empty Value With Select Options".to_owned()), value: None, - advanced: true, field_type: Some("select".to_owned()), select_options: Some(vec![IndexerSelectOption { value: Number::from(-2), @@ -2238,11 +2325,19 @@ mod test { name: Some("valueIsAnArray".to_owned()), label: Some("Value is an array".to_owned()), value: Some(json!([1, 2])), - advanced: false, field_type: Some("select".to_owned()), select_options: None, }, ]), } } + + fn indexer_settings() -> IndexerSettings { + IndexerSettings { + rss_sync_interval: Number::from(60), + allow_hardcoded_subs: true, + id: Number::from(1), + ..IndexerSettings::default() + } + } } diff --git a/src/ui/radarr_ui/indexers_ui.rs b/src/ui/radarr_ui/indexers_ui.rs index 3e2bcc0..665fd84 100644 --- a/src/ui/radarr_ui/indexers_ui.rs +++ b/src/ui/radarr_ui/indexers_ui.rs @@ -9,17 +9,24 @@ use crate::app::App; use crate::models::radarr_models::Indexer; use crate::models::Route; use crate::ui::utils::{layout_block_top_border, style_failure, style_primary, style_success}; -use crate::ui::{draw_table, DrawUi, TableProps}; +use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps}; pub(super) struct IndexersUi {} impl DrawUi for IndexersUi { fn draw(f: &mut Frame<'_, B>, app: &mut App<'_>, content_rect: Rect) { - if matches!( - *app.get_current_route(), - Route::Radarr(ActiveRadarrBlock::Indexers, _) - ) { - draw_indexers(f, app, content_rect); + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + match active_radarr_block { + ActiveRadarrBlock::Indexers => draw_indexers(f, app, content_rect), + ActiveRadarrBlock::DeleteIndexerPrompt => draw_prompt_popup_over( + f, + app, + content_rect, + draw_indexers, + draw_delete_indexer_prompt, + ), + _ => (), + } } } } @@ -94,3 +101,28 @@ fn draw_indexers(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect true, ) } + +fn draw_delete_indexer_prompt( + f: &mut Frame<'_, B>, + app: &mut App<'_>, + prompt_area: Rect, +) { + draw_prompt_box( + f, + prompt_area, + "Delete Indexer", + format!( + "Do you really want to delete this indexer: {}?", + app + .data + .radarr_data + .indexers + .current_selection() + .name + .clone() + .unwrap_or_default() + ) + .as_str(), + app.data.radarr_data.prompt_confirm, + ); +} diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index efd18d5..1cb71c8 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -11,8 +11,8 @@ use tui::Frame; use crate::app::radarr::{ ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, - EDIT_COLLECTION_BLOCKS, EDIT_MOVIE_BLOCKS, FILTER_BLOCKS, MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, - SYSTEM_DETAILS_BLOCKS, + EDIT_COLLECTION_BLOCKS, EDIT_MOVIE_BLOCKS, FILTER_BLOCKS, INDEXER_BLOCKS, MOVIE_DETAILS_BLOCKS, + SEARCH_BLOCKS, SYSTEM_DETAILS_BLOCKS, }; use crate::app::App; use crate::logos::RADARR_LOGO; @@ -74,7 +74,9 @@ impl DrawUi for RadarrUi { ActiveRadarrBlock::Downloads | ActiveRadarrBlock::DeleteDownloadPrompt | ActiveRadarrBlock::UpdateDownloadsPrompt => DownloadsUi::draw(f, app, content_rect), - ActiveRadarrBlock::Indexers => IndexersUi::draw(f, app, content_rect), + _ if INDEXER_BLOCKS.contains(&active_radarr_block) => { + IndexersUi::draw(f, app, content_rect) + } ActiveRadarrBlock::RootFolders | ActiveRadarrBlock::AddRootFolderPrompt | ActiveRadarrBlock::DeleteRootFolderPrompt => RootFoldersUi::draw(f, app, content_rect),