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

This commit is contained in:
2023-08-08 10:50:07 -06:00
parent 52f22312f3
commit 2b9ddd0d1e
11 changed files with 419 additions and 58 deletions
+5
View File
@@ -16,6 +16,7 @@ generate_keybindings! {
right, right,
backspace, backspace,
search, search,
settings,
filter, filter,
sort, sort,
edit, edit,
@@ -66,6 +67,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
key: Key::Char('s'), key: Key::Char('s'),
desc: "Search", desc: "Search",
}, },
settings: KeyBinding {
key: Key::Char('s'),
desc: "Settings",
},
filter: KeyBinding { filter: KeyBinding {
key: Key::Char('f'), key: Key::Char('f'),
desc: "Filter", desc: "Filter",
+22 -4
View File
@@ -5,8 +5,8 @@ use strum::IntoEnumIterator;
use crate::app::{App, Route}; use crate::app::{App, Route};
use crate::models::radarr_models::{ use crate::models::radarr_models::{
AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Indexer, AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Indexer,
MinimumAvailability, Monitor, Movie, MovieHistoryItem, QueueEvent, Release, ReleaseField, IndexerSettings, MinimumAvailability, Monitor, Movie, MovieHistoryItem, QueueEvent, Release,
RootFolder, Task, ReleaseField, RootFolder, Task,
}; };
use crate::models::{ use crate::models::{
BlockSelectionState, HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable, BlockSelectionState, HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable,
@@ -37,6 +37,7 @@ pub struct RadarrData<'a> {
pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>, pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>,
pub downloads: StatefulTable<DownloadRecord>, pub downloads: StatefulTable<DownloadRecord>,
pub indexers: StatefulTable<Indexer>, pub indexers: StatefulTable<Indexer>,
pub indexer_settings: Option<IndexerSettings>,
pub quality_profile_map: BiMap<u64, String>, pub quality_profile_map: BiMap<u64, String>,
pub tags_map: BiMap<u64, String>, pub tags_map: BiMap<u64, String>,
pub movie_details: ScrollableText, pub movie_details: ScrollableText,
@@ -263,6 +264,7 @@ impl<'a> Default for RadarrData<'a> {
filtered_movies: StatefulTable::default(), filtered_movies: StatefulTable::default(),
downloads: StatefulTable::default(), downloads: StatefulTable::default(),
indexers: StatefulTable::default(), indexers: StatefulTable::default(),
indexer_settings: None,
quality_profile_map: BiMap::default(), quality_profile_map: BiMap::default(),
tags_map: BiMap::default(), tags_map: BiMap::default(),
file_details: String::default(), file_details: String::default(),
@@ -324,7 +326,7 @@ impl<'a> Default for RadarrData<'a> {
title: "Indexers", title: "Indexers",
route: ActiveRadarrBlock::Indexers.into(), route: ActiveRadarrBlock::Indexers.into(),
help: "", help: "",
contextual_help: Some("<r> refresh"), contextual_help: Some("<enter> edit | <s> settings | <del> delete | <r> refresh"),
}, },
TabRoute { TabRoute {
title: "System", title: "System",
@@ -377,6 +379,7 @@ impl<'a> Default for RadarrData<'a> {
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ActiveRadarrBlock { pub enum ActiveRadarrBlock {
AddIndexer,
AddMovieAlreadyInLibrary, AddMovieAlreadyInLibrary,
AddMovieSearchInput, AddMovieSearchInput,
AddMovieSearchResults, AddMovieSearchResults,
@@ -394,11 +397,12 @@ pub enum ActiveRadarrBlock {
CollectionDetails, CollectionDetails,
Cast, Cast,
Crew, Crew,
DeleteDownloadPrompt,
DeleteIndexerPrompt,
DeleteMoviePrompt, DeleteMoviePrompt,
DeleteMovieConfirmPrompt, DeleteMovieConfirmPrompt,
DeleteMovieToggleDeleteFile, DeleteMovieToggleDeleteFile,
DeleteMovieToggleAddListExclusion, DeleteMovieToggleAddListExclusion,
DeleteDownloadPrompt,
DeleteRootFolderPrompt, DeleteRootFolderPrompt,
Downloads, Downloads,
EditCollectionPrompt, EditCollectionPrompt,
@@ -408,6 +412,7 @@ pub enum ActiveRadarrBlock {
EditCollectionSelectQualityProfile, EditCollectionSelectQualityProfile,
EditCollectionToggleSearchOnAdd, EditCollectionToggleSearchOnAdd,
EditCollectionToggleMonitored, EditCollectionToggleMonitored,
EditIndexer,
EditMoviePrompt, EditMoviePrompt,
EditMovieConfirmPrompt, EditMovieConfirmPrompt,
EditMoviePathInput, EditMoviePathInput,
@@ -419,6 +424,7 @@ pub enum ActiveRadarrBlock {
FilterCollections, FilterCollections,
FilterMovies, FilterMovies,
Indexers, Indexers,
IndexerSettings,
ManualSearch, ManualSearch,
ManualSearchSortPrompt, ManualSearchSortPrompt,
ManualSearchConfirmPrompt, ManualSearchConfirmPrompt,
@@ -531,6 +537,13 @@ pub static DELETE_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::DeleteMovieToggleAddListExclusion, ActiveRadarrBlock::DeleteMovieToggleAddListExclusion,
ActiveRadarrBlock::DeleteMovieConfirmPrompt, 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] = [ pub static SYSTEM_DETAILS_BLOCKS: [ActiveRadarrBlock; 5] = [
ActiveRadarrBlock::SystemLogs, ActiveRadarrBlock::SystemLogs,
ActiveRadarrBlock::SystemQueuedEvents, ActiveRadarrBlock::SystemQueuedEvents,
@@ -587,6 +600,11 @@ impl<'a> App<'a> {
.dispatch_network_event(RadarrEvent::GetIndexers.into()) .dispatch_network_event(RadarrEvent::GetIndexers.into())
.await; .await;
} }
ActiveRadarrBlock::IndexerSettings => {
self
.dispatch_network_event(RadarrEvent::GetIndexerSettings.into())
.await;
}
ActiveRadarrBlock::System => { ActiveRadarrBlock::System => {
self self
.dispatch_network_event(RadarrEvent::GetTasks.into()) .dispatch_network_event(RadarrEvent::GetTasks.into())
+19 -1
View File
@@ -274,6 +274,7 @@ mod tests {
assert!(radarr_data.filtered_movies.items.is_empty()); assert!(radarr_data.filtered_movies.items.is_empty());
assert!(radarr_data.downloads.items.is_empty()); assert!(radarr_data.downloads.items.is_empty());
assert!(radarr_data.indexers.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.quality_profile_map.is_empty());
assert!(radarr_data.tags_map.is_empty()); assert!(radarr_data.tags_map.is_empty());
assert!(radarr_data.file_details.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!(radarr_data.main_tabs.tabs[4].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[4].contextual_help, radarr_data.main_tabs.tabs[4].contextual_help,
Some("<r> refresh") Some("<enter> edit | <s> settings | <del> delete | <r> refresh")
); );
assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System"); assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System");
@@ -713,6 +714,23 @@ mod tests {
assert_eq!(app.tick_count, 0); 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] #[tokio::test]
async fn test_dispatch_by_system_block() { async fn test_dispatch_by_system_block() {
let (mut app, mut sync_network_rx) = construct_app_unit(); let (mut app, mut sync_network_rx) = construct_app_unit();
+27
View File
@@ -251,6 +251,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
ActiveRadarrBlock::RootFolders => self ActiveRadarrBlock::RootFolders => self
.app .app
.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into()), .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::DeleteDownloadPrompt
| ActiveRadarrBlock::DeleteIndexerPrompt
| ActiveRadarrBlock::DeleteRootFolderPrompt | ActiveRadarrBlock::DeleteRootFolderPrompt
| ActiveRadarrBlock::UpdateAllMoviesPrompt | ActiveRadarrBlock::UpdateAllMoviesPrompt
| ActiveRadarrBlock::UpdateAllCollectionsPrompt | ActiveRadarrBlock::UpdateAllCollectionsPrompt
@@ -404,6 +408,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
self.app.pop_navigation_stack(); 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 => { ActiveRadarrBlock::UpdateAllMoviesPrompt => {
if self.app.data.radarr_data.prompt_confirm { if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies); 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.should_ignore_quit_key = false;
self.app.pop_navigation_stack(); 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; self.app.should_ignore_quit_key = false;
} }
ActiveRadarrBlock::DeleteDownloadPrompt ActiveRadarrBlock::DeleteDownloadPrompt
| ActiveRadarrBlock::DeleteIndexerPrompt
| ActiveRadarrBlock::DeleteRootFolderPrompt | ActiveRadarrBlock::DeleteRootFolderPrompt
| ActiveRadarrBlock::UpdateAllMoviesPrompt | ActiveRadarrBlock::UpdateAllMoviesPrompt
| ActiveRadarrBlock::UpdateAllCollectionsPrompt | ActiveRadarrBlock::UpdateAllCollectionsPrompt
@@ -527,9 +544,19 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
_ => (), _ => (),
}, },
ActiveRadarrBlock::Indexers => match self.key { 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 => { _ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if *key == DEFAULT_KEYBINDINGS.settings.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::IndexerSettings.into());
}
_ => (), _ => (),
}, },
ActiveRadarrBlock::Collections => match self.key { ActiveRadarrBlock::Collections => match self.key {
@@ -135,4 +135,21 @@ mod utils {
assert_eq!(app.data.radarr_data.edit_search_on_add, Some(true)); 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());
};
}
} }
@@ -209,6 +209,8 @@ mod tests {
mod test_handle_delete { mod test_handle_delete {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::assert_delete_prompt;
use super::*; use super::*;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
@@ -217,11 +219,10 @@ mod tests {
fn test_movies_delete() { fn test_movies_delete() {
let mut app = App::default(); let mut app = App::default();
RadarrHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); assert_delete_prompt!(
app,
assert_eq!( ActiveRadarrBlock::Movies,
app.get_current_route(), ActiveRadarrBlock::DeleteMoviePrompt
&ActiveRadarrBlock::DeleteMoviePrompt.into()
); );
assert_eq!( assert_eq!(
app.data.radarr_data.selected_block.get_active_block(), app.data.radarr_data.selected_block.get_active_block(),
@@ -229,34 +230,18 @@ mod tests {
); );
} }
#[test] #[rstest]
fn test_downloads_delete() { #[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::DeleteDownloadPrompt)]
let mut app = App::default(); #[case(
ActiveRadarrBlock::RootFolders,
RadarrHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); ActiveRadarrBlock::DeleteRootFolderPrompt
)]
assert_eq!( #[case(ActiveRadarrBlock::Indexers, ActiveRadarrBlock::DeleteIndexerPrompt)]
app.get_current_route(), fn test_delete_prompt(
&ActiveRadarrBlock::DeleteDownloadPrompt.into() #[case] active_radarr_block: ActiveRadarrBlock,
); #[case] expected_radarr_block: ActiveRadarrBlock,
} ) {
assert_delete_prompt!(active_radarr_block, expected_radarr_block);
#[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()
);
} }
} }
@@ -332,6 +317,7 @@ mod tests {
fn test_left_right_prompt_toggle( fn test_left_right_prompt_toggle(
#[values( #[values(
ActiveRadarrBlock::DeleteDownloadPrompt, ActiveRadarrBlock::DeleteDownloadPrompt,
ActiveRadarrBlock::DeleteIndexerPrompt,
ActiveRadarrBlock::DeleteRootFolderPrompt, ActiveRadarrBlock::DeleteRootFolderPrompt,
ActiveRadarrBlock::UpdateAllMoviesPrompt, ActiveRadarrBlock::UpdateAllMoviesPrompt,
ActiveRadarrBlock::UpdateAllCollectionsPrompt, ActiveRadarrBlock::UpdateAllCollectionsPrompt,
@@ -387,6 +373,18 @@ mod tests {
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; 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] #[rstest]
#[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::MovieDetails)] #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::MovieDetails)]
#[case(ActiveRadarrBlock::Collections, ActiveRadarrBlock::CollectionDetails)] #[case(ActiveRadarrBlock::Collections, ActiveRadarrBlock::CollectionDetails)]
@@ -629,6 +627,11 @@ mod tests {
ActiveRadarrBlock::DeleteDownloadPrompt, ActiveRadarrBlock::DeleteDownloadPrompt,
RadarrEvent::DeleteDownload RadarrEvent::DeleteDownload
)] )]
#[case(
ActiveRadarrBlock::Indexers,
ActiveRadarrBlock::DeleteIndexerPrompt,
RadarrEvent::DeleteIndexer
)]
#[case( #[case(
ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::RootFolders,
ActiveRadarrBlock::DeleteRootFolderPrompt, ActiveRadarrBlock::DeleteRootFolderPrompt,
@@ -751,6 +754,7 @@ mod tests {
ActiveRadarrBlock::DeleteRootFolderPrompt ActiveRadarrBlock::DeleteRootFolderPrompt
)] )]
#[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::DeleteDownloadPrompt)] #[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::DeleteDownloadPrompt)]
#[case(ActiveRadarrBlock::Indexers, ActiveRadarrBlock::DeleteIndexerPrompt)]
#[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::UpdateDownloadsPrompt)] #[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::UpdateDownloadsPrompt)]
#[case( #[case(
ActiveRadarrBlock::Collections, ActiveRadarrBlock::Collections,
@@ -899,6 +903,24 @@ mod tests {
assert!(app.should_ignore_quit_key); 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] #[test]
fn test_root_folder_add() { fn test_root_folder_add() {
let mut app = App::default(); 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] #[test]
fn test_add_root_folder_prompt_backspace_key() { fn test_add_root_folder_prompt_backspace_key() {
let mut app = App::default(); let mut app = App::default();
+21 -1
View File
@@ -182,7 +182,6 @@ pub struct IndexerField {
pub value: Option<Value>, pub value: Option<Value>,
#[serde(rename(deserialize = "type"))] #[serde(rename(deserialize = "type"))]
pub field_type: Option<String>, pub field_type: Option<String>,
pub advanced: bool,
pub select_options: Option<Vec<IndexerSelectOption>>, pub select_options: Option<Vec<IndexerSelectOption>>,
} }
@@ -197,6 +196,27 @@ pub struct IndexerSelectOption {
pub order: Number, 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)] #[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Language { pub struct Language {
pub name: String, pub name: String,
+91 -4
View File
@@ -11,8 +11,8 @@ use crate::app::RadarrConfig;
use crate::models::radarr_models::{ use crate::models::radarr_models::{
AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie, AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie,
CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Indexer, CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Indexer,
LogResponse, Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, QueueEvent, Release, IndexerSettings, LogResponse, Movie, MovieCommandBody, MovieHistoryItem, QualityProfile,
ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, Update, QueueEvent, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, Update,
}; };
use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText}; use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText};
use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps}; use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps};
@@ -27,6 +27,7 @@ pub enum RadarrEvent {
AddMovie, AddMovie,
AddRootFolder, AddRootFolder,
DeleteDownload, DeleteDownload,
DeleteIndexer,
DeleteMovie, DeleteMovie,
DeleteRootFolder, DeleteRootFolder,
DownloadRelease, DownloadRelease,
@@ -35,6 +36,7 @@ pub enum RadarrEvent {
GetCollections, GetCollections,
GetDownloads, GetDownloads,
GetIndexers, GetIndexers,
GetIndexerSettings,
GetLogs, GetLogs,
GetMovieCredits, GetMovieCredits,
GetMovieDetails, GetMovieDetails,
@@ -57,6 +59,7 @@ pub enum RadarrEvent {
UpdateAndScan, UpdateAndScan,
UpdateCollections, UpdateCollections,
UpdateDownloads, UpdateDownloads,
UpdateIndexerSettings,
} }
impl RadarrEvent { impl RadarrEvent {
@@ -64,7 +67,8 @@ impl RadarrEvent {
match self { match self {
RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection",
RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue",
RadarrEvent::GetIndexers => "/indexer", RadarrEvent::GetIndexers | RadarrEvent::DeleteIndexer => "/indexer",
RadarrEvent::GetIndexerSettings | RadarrEvent::UpdateIndexerSettings => "/config/indexer",
RadarrEvent::GetLogs => "/log", RadarrEvent::GetLogs => "/log",
RadarrEvent::AddMovie RadarrEvent::AddMovie
| RadarrEvent::EditMovie | RadarrEvent::EditMovie
@@ -107,8 +111,9 @@ impl<'a, 'b> Network<'a, 'b> {
match radarr_event { match radarr_event {
RadarrEvent::AddMovie => self.add_movie().await, RadarrEvent::AddMovie => self.add_movie().await,
RadarrEvent::AddRootFolder => self.add_root_folder().await, RadarrEvent::AddRootFolder => self.add_root_folder().await,
RadarrEvent::DeleteMovie => self.delete_movie().await,
RadarrEvent::DeleteDownload => self.delete_download().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::DeleteRootFolder => self.delete_root_folder().await,
RadarrEvent::DownloadRelease => self.download_release().await, RadarrEvent::DownloadRelease => self.download_release().await,
RadarrEvent::EditCollection => self.edit_collection().await, RadarrEvent::EditCollection => self.edit_collection().await,
@@ -116,6 +121,7 @@ impl<'a, 'b> Network<'a, 'b> {
RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetCollections => self.get_collections().await,
RadarrEvent::GetDownloads => self.get_downloads().await, RadarrEvent::GetDownloads => self.get_downloads().await,
RadarrEvent::GetIndexers => self.get_indexers().await, RadarrEvent::GetIndexers => self.get_indexers().await,
RadarrEvent::GetIndexerSettings => self.get_indexer_settings().await,
RadarrEvent::GetLogs => self.get_logs().await, RadarrEvent::GetLogs => self.get_logs().await,
RadarrEvent::GetMovieCredits => self.get_credits().await, RadarrEvent::GetMovieCredits => self.get_credits().await,
RadarrEvent::GetMovieDetails => self.get_movie_details().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::UpdateAndScan => self.update_and_scan().await,
RadarrEvent::UpdateCollections => self.update_collections().await, RadarrEvent::UpdateCollections => self.update_collections().await,
RadarrEvent::UpdateDownloads => self.update_downloads().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; .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) { async fn delete_movie(&self) {
let movie_id = self.extract_movie_id().await; let movie_id = self.extract_movie_id().await;
let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files;
@@ -644,6 +682,24 @@ impl<'a, 'b> Network<'a, 'b> {
.await .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) { async fn get_healthcheck(&self) {
info!("Performing Radarr health check"); info!("Performing Radarr health check");
@@ -1302,6 +1358,37 @@ impl<'a, 'b> Network<'a, 'b> {
.await; .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::<IndexerSettings, Value>(request_props, |_, _| {})
.await;
self.app.lock().await.data.radarr_data.indexer_settings = None;
}
async fn radarr_request_props_from<T: Serialize + Debug>( async fn radarr_request_props_from<T: Serialize + Debug>(
&self, &self,
resource: &str, resource: &str,
+101 -6
View File
@@ -531,6 +531,44 @@ mod test {
async_server.assert_async().await; 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] #[tokio::test]
async fn test_handle_update_collections_event() { async fn test_handle_update_collections_event() {
let (async_server, app_arc, _server) = mock_radarr_api( let (async_server, app_arc, _server) = mock_radarr_api(
@@ -873,13 +911,11 @@ mod test {
"label": "Value Is String", "label": "Value Is String",
"value": "hello", "value": "hello",
"type": "textbox", "type": "textbox",
"advanced": false
}, },
{ {
"order": 1, "order": 1,
"name": "emptyValueWithSelectOptions", "name": "emptyValueWithSelectOptions",
"label": "Empty Value With Select Options", "label": "Empty Value With Select Options",
"advanced": true,
"type": "select", "type": "select",
"selectOptions": [ "selectOptions": [
{ {
@@ -895,7 +931,6 @@ mod test {
"label": "Value is an array", "label": "Value is an array",
"value": [1, 2], "value": [1, 2],
"type": "select", "type": "select",
"advanced": false,
}, },
], ],
"implementationName": "Torznab", "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] #[tokio::test]
async fn test_handle_get_queued_events_event() { async fn test_handle_get_queued_events_event() {
let queued_events_json = json!([{ let queued_events_json = json!([{
@@ -1362,6 +1430,27 @@ mod test {
async_server.assert_async().await; 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] #[tokio::test]
async fn test_handle_delete_root_folder_event() { async fn test_handle_delete_root_folder_event() {
let resource = format!("{}/1", RadarrEvent::DeleteRootFolder.resource()); let resource = format!("{}/1", RadarrEvent::DeleteRootFolder.resource());
@@ -2216,7 +2305,6 @@ mod test {
name: Some("valueIsString".to_owned()), name: Some("valueIsString".to_owned()),
label: Some("Value Is String".to_owned()), label: Some("Value Is String".to_owned()),
value: Some(json!("hello")), value: Some(json!("hello")),
advanced: false,
field_type: Some("textbox".to_owned()), field_type: Some("textbox".to_owned()),
select_options: None, select_options: None,
}, },
@@ -2225,7 +2313,6 @@ mod test {
name: Some("emptyValueWithSelectOptions".to_owned()), name: Some("emptyValueWithSelectOptions".to_owned()),
label: Some("Empty Value With Select Options".to_owned()), label: Some("Empty Value With Select Options".to_owned()),
value: None, value: None,
advanced: true,
field_type: Some("select".to_owned()), field_type: Some("select".to_owned()),
select_options: Some(vec![IndexerSelectOption { select_options: Some(vec![IndexerSelectOption {
value: Number::from(-2), value: Number::from(-2),
@@ -2238,11 +2325,19 @@ mod test {
name: Some("valueIsAnArray".to_owned()), name: Some("valueIsAnArray".to_owned()),
label: Some("Value is an array".to_owned()), label: Some("Value is an array".to_owned()),
value: Some(json!([1, 2])), value: Some(json!([1, 2])),
advanced: false,
field_type: Some("select".to_owned()), field_type: Some("select".to_owned()),
select_options: None, select_options: None,
}, },
]), ]),
} }
} }
fn indexer_settings() -> IndexerSettings {
IndexerSettings {
rss_sync_interval: Number::from(60),
allow_hardcoded_subs: true,
id: Number::from(1),
..IndexerSettings::default()
}
}
} }
+38 -6
View File
@@ -9,17 +9,24 @@ use crate::app::App;
use crate::models::radarr_models::Indexer; use crate::models::radarr_models::Indexer;
use crate::models::Route; use crate::models::Route;
use crate::ui::utils::{layout_block_top_border, style_failure, style_primary, style_success}; 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 {} pub(super) struct IndexersUi {}
impl DrawUi for IndexersUi { impl DrawUi for IndexersUi {
fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, content_rect: Rect) { fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, content_rect: Rect) {
if matches!( if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
*app.get_current_route(), match active_radarr_block {
Route::Radarr(ActiveRadarrBlock::Indexers, _) ActiveRadarrBlock::Indexers => draw_indexers(f, app, content_rect),
) { ActiveRadarrBlock::DeleteIndexerPrompt => draw_prompt_popup_over(
draw_indexers(f, app, content_rect); f,
app,
content_rect,
draw_indexers,
draw_delete_indexer_prompt,
),
_ => (),
}
} }
} }
} }
@@ -94,3 +101,28 @@ fn draw_indexers<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect
true, true,
) )
} }
fn draw_delete_indexer_prompt<B: Backend>(
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,
);
}
+5 -3
View File
@@ -11,8 +11,8 @@ use tui::Frame;
use crate::app::radarr::{ use crate::app::radarr::{
ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS,
EDIT_COLLECTION_BLOCKS, EDIT_MOVIE_BLOCKS, FILTER_BLOCKS, MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_MOVIE_BLOCKS, FILTER_BLOCKS, INDEXER_BLOCKS, MOVIE_DETAILS_BLOCKS,
SYSTEM_DETAILS_BLOCKS, SEARCH_BLOCKS, SYSTEM_DETAILS_BLOCKS,
}; };
use crate::app::App; use crate::app::App;
use crate::logos::RADARR_LOGO; use crate::logos::RADARR_LOGO;
@@ -74,7 +74,9 @@ impl DrawUi for RadarrUi {
ActiveRadarrBlock::Downloads ActiveRadarrBlock::Downloads
| ActiveRadarrBlock::DeleteDownloadPrompt | ActiveRadarrBlock::DeleteDownloadPrompt
| ActiveRadarrBlock::UpdateDownloadsPrompt => DownloadsUi::draw(f, app, content_rect), | 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::RootFolders
| ActiveRadarrBlock::AddRootFolderPrompt | ActiveRadarrBlock::AddRootFolderPrompt
| ActiveRadarrBlock::DeleteRootFolderPrompt => RootFoldersUi::draw(f, app, content_rect), | ActiveRadarrBlock::DeleteRootFolderPrompt => RootFoldersUi::draw(f, app, content_rect),