diff --git a/README.md b/README.md index 73067db5..9497ecee 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built - [x] ![radarr_logo](logos/radarr.png) [Radarr](https://wiki.servarr.com/radarr) - [x] ![sonarr_logo](logos/sonarr.png) [Sonarr](https://wiki.servarr.com/en/sonarr) - [x] ![lidarr_logo](logos/lidarr.png) [Lidarr](https://wiki.servarr.com/en/lidarr) -- [ ] ![readarr_logo](logos/readarr.png) [Readarr](https://wiki.servarr.com/en/readarr) +- [x] ![readarr_logo](logos/readarr.png) [Readarr](https://wiki.servarr.com/en/readarr) - [ ] ![prowlarr_logo](logos/prowlarr.png) [Prowlarr](https://wiki.servarr.com/en/prowlarr) - [ ] ![whisparr_logo](logos/whisparr.png) [Whisparr](https://wiki.servarr.com/whisparr) - [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/) @@ -256,6 +256,7 @@ Commands: radarr Commands for manging your Radarr instance sonarr Commands for manging your Sonarr instance lidarr Commands for manging your Lidarr instance + readarr Commands for managing your Readarr instance completions Generate shell completions for the Managarr CLI tail-logs Tail Managarr logs config-path Print the full path to the default configuration file. @@ -347,6 +348,7 @@ default port for that respective service. That is: | Radarr | `localhost` | 7878 | | Sonarr | `localhost` | 8989 | | Lidarr | `localhost` | 8686 | +| Readarr | `localhost` | 8787 | > [!TIP] > In general, all Servarrs store their API tokens under Settings -> General -> Security -> API Key in their web UIs. @@ -367,6 +369,10 @@ sonarr: lidarr: # Connect to lidarr running on localhost:8686 - api_token: + +readarr: + # Connect to readarr running on localhost:8787 + - api_token: ``` ## Configuration File Location @@ -591,4 +597,4 @@ Managarr supports using environment variables on startup so you don't have to al * [Tautulli >= v2](https://github.com/Tautulli/Tautulli/wiki/Tautulli-API-Reference) ## Creator -* [Alex Clarke](https://github.com/Dark-Alex-17) +* [Alex Clarke](https://github.com/Dark-Alex-17) \ No newline at end of file diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index ffefe313..c9ae7f18 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -11,6 +11,7 @@ mod tests { use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars}; use crate::models::servarr_data::lidarr::lidarr_data::LidarrData; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; + use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::{HorizontallyScrollableText, TabRoute}; use crate::network::NetworkEvent; @@ -33,11 +34,18 @@ mod tests { ..ServarrConfig::default() }; let sonarr_config_2 = ServarrConfig::default(); + let readarr_config_1 = ServarrConfig { + name: Some("Readarr Test".to_owned()), + weight: Some(2), + ..ServarrConfig::default() + }; + let readarr_config_2 = ServarrConfig::default(); let config = AppConfig { theme: None, radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]), sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]), lidarr: None, + readarr: Some(vec![readarr_config_1.clone(), readarr_config_2.clone()]), }; let expected_tab_routes = vec![ TabRoute { @@ -46,6 +54,12 @@ mod tests { contextual_help: None, config: Some(sonarr_config_1), }, + TabRoute { + title: "Readarr Test".to_owned(), + route: ActiveReadarrBlock::default().into(), + contextual_help: None, + config: Some(readarr_config_1), + }, TabRoute { title: "Radarr 1".to_owned(), route: ActiveRadarrBlock::default().into(), @@ -64,6 +78,12 @@ mod tests { contextual_help: None, config: Some(sonarr_config_2), }, + TabRoute { + title: "Readarr 1".to_owned(), + route: ActiveReadarrBlock::default().into(), + contextual_help: None, + config: Some(readarr_config_2), + }, ]; let app = App::new( @@ -189,6 +209,7 @@ mod tests { let data = Data { lidarr_data: LidarrData::default(), radarr_data, + readarr_data: ReadarrData::default(), sonarr_data, }; let mut app = App { diff --git a/src/app/context_clues.rs b/src/app/context_clues.rs index d7ec1b2e..f870f1c6 100644 --- a/src/app/context_clues.rs +++ b/src/app/context_clues.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding}; use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider; use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider; +use crate::app::readarr::readarr_context_clues::ReadarrContextClueProvider; use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider; use crate::models::Route; @@ -23,6 +24,7 @@ impl ContextClueProvider for ServarrContextClueProvider { Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app), Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app), Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app), + Route::Readarr(_, _) => ReadarrContextClueProvider::get_context_clues(app), _ => None, } } diff --git a/src/app/context_clues_tests.rs b/src/app/context_clues_tests.rs index a10881e9..a1a6f244 100644 --- a/src/app/context_clues_tests.rs +++ b/src/app/context_clues_tests.rs @@ -9,6 +9,7 @@ mod test { use crate::app::{App, key_binding::DEFAULT_KEYBINDINGS}; use crate::models::servarr_data::ActiveKeybindingBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; #[test] @@ -303,6 +304,16 @@ mod test { assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,); } + #[test] + fn test_servarr_context_clue_provider_delegates_to_readarr_provider() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::SystemTasks.into()); + + let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,); + } + #[test] fn test_servarr_context_clue_provider_unsupported_route_returns_none() { let mut app = App::test_default(); diff --git a/src/app/mod.rs b/src/app/mod.rs index a660243d..14b18339 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -17,6 +17,7 @@ use crate::cli::Command; use crate::models::servarr_data::Notification; use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; +use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_models::KeybindingItem; use crate::models::stateful_table::StatefulTable; @@ -30,6 +31,7 @@ pub mod key_binding; mod key_binding_tests; pub mod lidarr; pub mod radarr; +pub mod readarr; pub mod sonarr; pub struct App<'a> { @@ -122,6 +124,26 @@ impl App<'_> { server_tabs.extend(lidarr_tabs); } + if let Some(readarr_configs) = config.readarr { + let mut unnamed_idx = 0; + let readarr_tabs = readarr_configs.into_iter().map(|readarr_config| { + let name = if let Some(name) = readarr_config.name.clone() { + name + } else { + unnamed_idx += 1; + format!("Readarr {unnamed_idx}") + }; + + TabRoute { + title: name, + route: ActiveReadarrBlock::Authors.into(), + contextual_help: None, + config: Some(readarr_config), + } + }); + server_tabs.extend(readarr_tabs); + } + let weight_sorted_tabs = server_tabs .into_iter() .sorted_by(|tab1, tab2| { @@ -204,6 +226,7 @@ impl App<'_> { Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await, Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await, Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await, + Route::Readarr(active_readarr_block, _) => self.readarr_on_tick(active_readarr_block).await, _ => (), } @@ -300,6 +323,12 @@ impl App<'_> { contextual_help: None, config: Some(ServarrConfig::default()), }, + TabRoute { + title: "Readarr".to_owned(), + route: ActiveReadarrBlock::Authors.into(), + contextual_help: None, + config: Some(ServarrConfig::default()), + }, ]), ..App::default() } @@ -310,6 +339,7 @@ impl App<'_> { data: Data { lidarr_data: LidarrData::test_default_fully_populated(), radarr_data: RadarrData::test_default_fully_populated(), + readarr_data: ReadarrData::test_default_fully_populated(), sonarr_data: SonarrData::test_default_fully_populated(), }, server_tabs: TabState::new(vec![ @@ -331,6 +361,12 @@ impl App<'_> { contextual_help: None, config: Some(ServarrConfig::default()), }, + TabRoute { + title: "Readarr".to_owned(), + route: ActiveReadarrBlock::Authors.into(), + contextual_help: None, + config: Some(ServarrConfig::default()), + }, ]), ..App::default() } @@ -341,6 +377,7 @@ impl App<'_> { pub struct Data<'a> { pub lidarr_data: LidarrData<'a>, pub radarr_data: RadarrData<'a>, + pub readarr_data: ReadarrData<'a>, pub sonarr_data: SonarrData<'a>, } @@ -349,12 +386,17 @@ pub struct AppConfig { pub theme: Option, pub lidarr: Option>, pub radarr: Option>, + pub readarr: Option>, pub sonarr: Option>, } impl AppConfig { pub fn validate(&self, config_path: &str) { - if self.lidarr.is_none() && self.radarr.is_none() && self.sonarr.is_none() { + if self.lidarr.is_none() + && self.radarr.is_none() + && self.sonarr.is_none() + && self.readarr.is_none() + { log_and_print_error(format!( "No Servarrs are configured in the file: {config_path}" )); @@ -372,6 +414,10 @@ impl AppConfig { if let Some(lidarr_configs) = &self.lidarr { lidarr_configs.iter().for_each(|config| config.validate()); } + + if let Some(readarr_configs) = &self.readarr { + readarr_configs.iter().for_each(|config| config.validate()); + } } pub fn verify_config_present_for_cli(&self, command: &Command) { @@ -393,6 +439,10 @@ impl AppConfig { msg("Lidarr"); process::exit(1); } + Command::Readarr(_) if self.readarr.is_none() => { + msg("Readarr"); + process::exit(1); + } _ => (), } } @@ -415,6 +465,12 @@ impl AppConfig { lidarr_config.post_process_initialization(); } } + + if let Some(readarr_configs) = self.readarr.as_mut() { + for readarr_config in readarr_configs { + readarr_config.post_process_initialization(); + } + } } } diff --git a/src/app/readarr/mod.rs b/src/app/readarr/mod.rs new file mode 100644 index 00000000..70f89f2c --- /dev/null +++ b/src/app/readarr/mod.rs @@ -0,0 +1,48 @@ +use super::App; +use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; + +pub mod readarr_context_clues; + +#[cfg(test)] +#[path = "readarr_tests.rs"] +mod readarr_tests; + +impl App<'_> { + pub(super) async fn dispatch_by_readarr_block( + &mut self, + _active_readarr_block: &ActiveReadarrBlock, + ) { + self.check_for_readarr_prompt_action().await; + self.reset_tick_count(); + } + + async fn check_for_readarr_prompt_action(&mut self) { + if self.data.readarr_data.prompt_confirm { + self.data.readarr_data.prompt_confirm = false; + if let Some(readarr_event) = self.data.readarr_data.prompt_confirm_action.take() { + self.dispatch_network_event(readarr_event.into()).await; + self.should_refresh = true; + } + } + } + + pub(super) async fn readarr_on_tick(&mut self, active_readarr_block: ActiveReadarrBlock) { + if self.is_first_render { + self.dispatch_by_readarr_block(&active_readarr_block).await; + self.is_first_render = false; + return; + } + + if self.should_refresh { + self.dispatch_by_readarr_block(&active_readarr_block).await; + } + + if self.is_routing { + if !self.should_refresh { + self.cancellation_token.cancel(); + } else { + self.dispatch_by_readarr_block(&active_readarr_block).await; + } + } + } +} diff --git a/src/app/readarr/readarr_context_clues.rs b/src/app/readarr/readarr_context_clues.rs new file mode 100644 index 00000000..44f3a2bf --- /dev/null +++ b/src/app/readarr/readarr_context_clues.rs @@ -0,0 +1,205 @@ +use crate::app::App; +use crate::app::context_clues::{ + BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, +}; +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::models::Route; +use crate::models::servarr_data::readarr::readarr_data::{ + ADD_AUTHOR_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, + BOOK_DETAILS_BLOCKS, EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS, EDITION_DETAILS_BLOCKS, + INDEXER_SETTINGS_BLOCKS, +}; + +#[cfg(test)] +#[path = "readarr_context_clues_tests.rs"] +mod readarr_context_clues_tests; + +pub static AUTHORS_CONTEXT_CLUES: [ContextClue; 10] = [ + (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), + ( + DEFAULT_KEYBINDINGS.toggle_monitoring, + DEFAULT_KEYBINDINGS.toggle_monitoring.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.update, "update all"), + (DEFAULT_KEYBINDINGS.esc, "cancel filter"), +]; + +pub static ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [ + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, "edit search"), +]; + +pub static AUTHOR_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, "edit author"), + (DEFAULT_KEYBINDINGS.delete, "delete book"), + ( + DEFAULT_KEYBINDINGS.toggle_monitoring, + "toggle book monitoring", + ), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static AUTHOR_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, "edit author"), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), +]; + +pub static MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, "edit author"), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static BOOK_DETAILS_CONTEXT_CLUES: [ContextClue; 6] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), + (DEFAULT_KEYBINDINGS.submit, "edition details"), + (DEFAULT_KEYBINDINGS.delete, "delete book file"), +]; + +pub static BOOK_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), +]; + +pub static MANUAL_BOOK_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static EDITION_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub(in crate::app) struct ReadarrContextClueProvider; + +impl ContextClueProvider for ReadarrContextClueProvider { + fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> { + let Route::Readarr(active_readarr_block, _context_option) = app.get_current_route() else { + panic!("ReadarrContextClueProvider::get_context_clues called with non-Readarr route"); + }; + + match active_readarr_block { + _ if AUTHOR_DETAILS_BLOCKS.contains(&active_readarr_block) => app + .data + .readarr_data + .author_info_tabs + .get_active_route_contextual_help(), + _ if BOOK_DETAILS_BLOCKS.contains(&active_readarr_block) => app + .data + .readarr_data + .book_details_modal + .as_ref() + .expect("book_details_modal is empty") + .book_details_tabs + .get_active_route_contextual_help(), + _ if EDITION_DETAILS_BLOCKS.contains(&active_readarr_block) => { + Some(&EDITION_DETAILS_CONTEXT_CLUES) + } + ActiveReadarrBlock::AddAuthorSearchInput + | ActiveReadarrBlock::AddAuthorEmptySearchResults + | ActiveReadarrBlock::TestAllIndexers + | ActiveReadarrBlock::SystemLogs + | ActiveReadarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES), + _ if EDIT_AUTHOR_BLOCKS.contains(&active_readarr_block) + || EDIT_INDEXER_BLOCKS.contains(&active_readarr_block) + || INDEXER_SETTINGS_BLOCKS.contains(&active_readarr_block) + || ADD_ROOT_FOLDER_BLOCKS.contains(&active_readarr_block) => + { + Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES) + } + ActiveReadarrBlock::AddAuthorPrompt + | ActiveReadarrBlock::AddAuthorSelectMonitor + | ActiveReadarrBlock::AddAuthorSelectMonitorNewItems + | ActiveReadarrBlock::AddAuthorSelectQualityProfile + | ActiveReadarrBlock::AddAuthorSelectMetadataProfile + | ActiveReadarrBlock::AddAuthorSelectRootFolder + | ActiveReadarrBlock::AddAuthorTagsInput + | ActiveReadarrBlock::AddAuthorAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES), + _ if ADD_AUTHOR_BLOCKS.contains(&active_readarr_block) => { + Some(&ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES) + } + ActiveReadarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES), + _ => app + .data + .readarr_data + .main_tabs + .get_active_route_contextual_help(), + } + } +} diff --git a/src/app/readarr/readarr_context_clues_tests.rs b/src/app/readarr/readarr_context_clues_tests.rs new file mode 100644 index 00000000..b0ef4ec0 --- /dev/null +++ b/src/app/readarr/readarr_context_clues_tests.rs @@ -0,0 +1,540 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::context_clues::{ + BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, + }; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::readarr::readarr_context_clues::{ + ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES, AUTHOR_DETAILS_CONTEXT_CLUES, + AUTHOR_HISTORY_CONTEXT_CLUES, AUTHORS_CONTEXT_CLUES, BOOK_DETAILS_CONTEXT_CLUES, + BOOK_HISTORY_CONTEXT_CLUES, EDITION_DETAILS_CONTEXT_CLUES, MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES, + MANUAL_BOOK_SEARCH_CONTEXT_CLUES, ReadarrContextClueProvider, + }; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::servarr_data::readarr::modals::BookDetailsModal; + use crate::models::servarr_data::readarr::readarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ActiveReadarrBlock, EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS, + INDEXER_SETTINGS_BLOCKS, ReadarrData, + }; + use rstest::rstest; + + #[test] + fn test_authors_context_clues() { + let mut authors_context_clues_iter = AUTHORS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + authors_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc) + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.toggle_monitoring, + DEFAULT_KEYBINDINGS.toggle_monitoring.desc + ) + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc) + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc) + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.update, "update all") + ); + assert_some_eq_x!( + authors_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "cancel filter") + ); + assert_none!(authors_context_clues_iter.next()); + } + + #[test] + fn test_author_details_context_clues() { + let mut author_details_context_clues_iter = AUTHOR_DETAILS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + author_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ) + ); + assert_some_eq_x!( + author_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, "edit author") + ); + assert_some_eq_x!( + author_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.delete, "delete book") + ); + assert_some_eq_x!( + author_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.toggle_monitoring, + "toggle book monitoring", + ) + ); + assert_some_eq_x!( + author_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + author_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc) + ); + assert_some_eq_x!( + author_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ) + ); + assert_some_eq_x!( + author_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(author_details_context_clues_iter.next()); + } + + #[test] + fn test_add_author_search_results_context_clues() { + let mut add_author_search_results_context_clues_iter = + ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + add_author_search_results_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + add_author_search_results_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "edit search") + ); + assert_none!(add_author_search_results_context_clues_iter.next()); + } + + #[test] + #[should_panic( + expected = "ReadarrContextClueProvider::get_context_clues called with non-Readarr route" + )] + fn test_readarr_context_clue_provider_get_context_clues_non_readarr_route() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::default().into()); + + ReadarrContextClueProvider::get_context_clues(&mut app); + } + + #[test] + fn test_author_history_context_clues() { + let mut author_history_context_clues_iter = AUTHOR_HISTORY_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, "edit author") + ); + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc) + ); + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) + ); + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + author_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "cancel filter/close") + ); + assert_none!(author_history_context_clues_iter.next()); + } + + #[test] + fn test_manual_author_search_context_clues() { + let mut manual_author_search_context_clues_iter = MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + manual_author_search_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + manual_author_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, "edit author") + ); + assert_some_eq_x!( + manual_author_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + manual_author_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc) + ); + assert_some_eq_x!( + manual_author_search_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + manual_author_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + manual_author_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(manual_author_search_context_clues_iter.next()); + } + + #[test] + fn test_book_details_context_clues() { + let mut book_details_context_clues_iter = BOOK_DETAILS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + book_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + book_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + book_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + book_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_some_eq_x!( + book_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "edition details") + ); + assert_some_eq_x!( + book_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.delete, "delete book file") + ); + assert_none!(book_details_context_clues_iter.next()); + } + + #[test] + fn test_book_history_context_clues() { + let mut book_history_context_clues_iter = BOOK_HISTORY_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + book_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + book_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + book_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + book_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) + ); + assert_some_eq_x!( + book_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + book_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + book_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "cancel filter/close") + ); + assert_none!(book_history_context_clues_iter.next()); + } + + #[test] + fn test_manual_book_search_context_clues() { + let mut manual_book_search_context_clues_iter = MANUAL_BOOK_SEARCH_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + manual_book_search_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + manual_book_search_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + manual_book_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + manual_book_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + manual_book_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(manual_book_search_context_clues_iter.next()); + } + + #[test] + fn test_edition_details_context_clues() { + let mut edition_details_context_clues_iter = EDITION_DETAILS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + edition_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + edition_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(edition_details_context_clues_iter.next()); + } + + #[rstest] + #[case(0, ActiveReadarrBlock::AuthorDetails, &AUTHOR_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveReadarrBlock::AuthorHistory, &AUTHOR_HISTORY_CONTEXT_CLUES)] + #[case(2, ActiveReadarrBlock::ManualAuthorSearch, &MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES)] + fn test_readarr_context_clue_provider_author_info_tabs( + #[case] index: usize, + #[case] active_readarr_block: ActiveReadarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + app.data.readarr_data = ReadarrData::default(); + app.data.readarr_data.author_info_tabs.set_index(index); + app.push_navigation_stack(active_readarr_block.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, expected_context_clues); + } + + #[rstest] + #[case(0, ActiveReadarrBlock::BookDetails, &BOOK_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveReadarrBlock::BookHistory, &BOOK_HISTORY_CONTEXT_CLUES)] + #[case(2, ActiveReadarrBlock::ManualBookSearch, &MANUAL_BOOK_SEARCH_CONTEXT_CLUES)] + fn test_readarr_context_clue_provider_book_details_tabs( + #[case] index: usize, + #[case] active_readarr_block: ActiveReadarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + let mut book_details_modal = BookDetailsModal::default(); + book_details_modal.book_details_tabs.set_index(index); + let readarr_data = ReadarrData { + book_details_modal: Some(book_details_modal), + ..ReadarrData::default() + }; + app.data.readarr_data = readarr_data; + app.push_navigation_stack(active_readarr_block.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, expected_context_clues); + } + + #[test] + fn test_readarr_context_clue_provider_book_edition_details_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::BookEditionDetails.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &EDITION_DETAILS_CONTEXT_CLUES); + } + + #[test] + fn test_readarr_context_clue_provider_authors_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Authors.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES); + } + + #[test] + fn test_readarr_context_clue_provider_authors_sort_prompt_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::AuthorsSortPrompt.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES); + } + + #[test] + fn test_readarr_context_clue_provider_search_authors_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::SearchAuthors.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES); + } + + #[test] + fn test_readarr_context_clue_provider_filter_authors_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::FilterAuthors.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES); + } + + #[rstest] + fn test_readarr_context_clue_provider_bare_popup_context_clues( + #[values( + ActiveReadarrBlock::AddAuthorSearchInput, + ActiveReadarrBlock::AddAuthorEmptySearchResults, + ActiveReadarrBlock::TestAllIndexers, + ActiveReadarrBlock::SystemLogs, + ActiveReadarrBlock::SystemUpdates + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_readarr_block.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES); + } + + #[test] + fn test_readarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() { + let mut blocks = EDIT_AUTHOR_BLOCKS.to_vec(); + blocks.extend(ADD_ROOT_FOLDER_BLOCKS); + blocks.extend(INDEXER_SETTINGS_BLOCKS); + blocks.extend(EDIT_INDEXER_BLOCKS); + + for active_readarr_block in blocks { + let mut app = App::test_default(); + app.push_navigation_stack(active_readarr_block.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES); + } + } + + #[test] + fn test_readarr_context_clue_provider_add_author_search_results_context_clues() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::AddAuthorSearchResults.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES); + } + + #[rstest] + fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_add_author_blocks( + #[values( + ActiveReadarrBlock::AddAuthorPrompt, + ActiveReadarrBlock::AddAuthorSelectMonitor, + ActiveReadarrBlock::AddAuthorSelectMonitorNewItems, + ActiveReadarrBlock::AddAuthorSelectQualityProfile, + ActiveReadarrBlock::AddAuthorSelectMetadataProfile, + ActiveReadarrBlock::AddAuthorSelectRootFolder, + ActiveReadarrBlock::AddAuthorTagsInput, + ActiveReadarrBlock::AddAuthorAlreadyInLibrary + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_readarr_block.into()); + + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES); + } + + #[test] + fn test_readarr_context_clue_provider_system_tasks_clues() { + let mut app = App::test_default(); + + app.push_navigation_stack(ActiveReadarrBlock::SystemTasks.into()); + let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES); + } +} diff --git a/src/app/readarr/readarr_tests.rs b/src/app/readarr/readarr_tests.rs new file mode 100644 index 00000000..1aa8a0cc --- /dev/null +++ b/src/app/readarr/readarr_tests.rs @@ -0,0 +1,112 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; + use pretty_assertions::assert_eq; + + #[tokio::test] + async fn test_dispatch_by_readarr_block_resets_the_tick_count() { + let mut app = App { + tick_count: 2, + ..App::test_default() + }; + + app + .dispatch_by_readarr_block(&ActiveReadarrBlock::Authors) + .await; + + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_readarr_block_clears_the_pending_prompt_confirmation() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + + app + .dispatch_by_readarr_block(&ActiveReadarrBlock::Authors) + .await; + + assert!(!app.data.readarr_data.prompt_confirm); + assert!(!app.should_refresh); + } + + #[tokio::test] + async fn test_check_for_readarr_prompt_action_no_prompt_confirm() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = false; + + app.check_for_readarr_prompt_action().await; + + assert!(!app.data.readarr_data.prompt_confirm); + assert!(!app.should_refresh); + } + + #[tokio::test] + async fn test_check_for_readarr_prompt_action_with_no_prompt_confirm_action() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + + app.check_for_readarr_prompt_action().await; + + assert!(!app.data.readarr_data.prompt_confirm); + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + } + + #[tokio::test] + async fn test_readarr_on_tick_first_render() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + app.is_first_render = true; + + app.readarr_on_tick(ActiveReadarrBlock::Downloads).await; + + assert!(!app.data.readarr_data.prompt_confirm); + assert!(!app.is_first_render); + } + + #[tokio::test] + async fn test_readarr_on_tick_routing() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + app.is_routing = true; + app.should_refresh = true; + app.is_first_render = false; + app.tick_count = 1; + + app.readarr_on_tick(ActiveReadarrBlock::Downloads).await; + + assert!(!app.data.readarr_data.prompt_confirm); + assert!(!app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_readarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + app.is_routing = true; + app.should_refresh = false; + app.is_first_render = false; + app.tick_count = 1; + + app.readarr_on_tick(ActiveReadarrBlock::Downloads).await; + + assert!(app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_readarr_on_tick_should_refresh() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + app.should_refresh = true; + app.is_first_render = false; + app.tick_count = 1; + + app.readarr_on_tick(ActiveReadarrBlock::Downloads).await; + + assert!(app.should_refresh); + assert!(!app.data.readarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 858952bc..1492b53d 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -6,6 +6,7 @@ use clap_complete::Shell; use indoc::indoc; use lidarr::{LidarrCliHandler, LidarrCommand}; use radarr::{RadarrCliHandler, RadarrCommand}; +use readarr::{ReadarrCliHandler, ReadarrCommand}; use sonarr::{SonarrCliHandler, SonarrCommand}; use tokio::sync::Mutex; @@ -13,6 +14,7 @@ use crate::{app::App, network::NetworkTrait}; pub mod lidarr; pub mod radarr; +pub mod readarr; pub mod sonarr; #[cfg(test)] @@ -30,6 +32,9 @@ pub enum Command { #[command(subcommand, about = "Commands for manging your Lidarr instance")] Lidarr(LidarrCommand), + #[command(subcommand, about = "Commands for managing your Readarr instance")] + Readarr(ReadarrCommand), + #[command( arg_required_else_help = true, about = "Generate shell completions for the Managarr CLI" @@ -78,6 +83,11 @@ pub(crate) async fn handle_command( .handle() .await? } + Command::Readarr(readarr_command) => { + ReadarrCliHandler::with(app, readarr_command, network) + .handle() + .await? + } _ => String::new(), }; diff --git a/src/cli/readarr/mod.rs b/src/cli/readarr/mod.rs new file mode 100644 index 00000000..7797af94 --- /dev/null +++ b/src/cli/readarr/mod.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use super::{CliCommandHandler, Command}; +use crate::{app::App, network::NetworkTrait}; + +#[cfg(test)] +#[path = "readarr_command_tests.rs"] +mod readarr_command_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum ReadarrCommand {} + +impl From for Command { + fn from(readarr_command: ReadarrCommand) -> Command { + Command::Readarr(readarr_command) + } +} + +pub(super) struct ReadarrCliHandler<'a, 'b> { + app: &'a Arc>>, + command: ReadarrCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, 'b> { + fn with( + app: &'a Arc>>, + command: ReadarrCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + ReadarrCliHandler { + app, + command, + network, + } + } + + async fn handle(self) -> Result { + match self.command {} + } +} diff --git a/src/cli/readarr/readarr_command_tests.rs b/src/cli/readarr/readarr_command_tests.rs new file mode 100644 index 00000000..f07a7d9b --- /dev/null +++ b/src/cli/readarr/readarr_command_tests.rs @@ -0,0 +1,15 @@ +#[cfg(test)] +mod tests { + mod cli { + use clap::CommandFactory; + + use crate::Cli; + + #[test] + fn test_readarr_command_requires_a_subcommand() { + let result = Cli::command().try_get_matches_from(["managarr", "readarr"]); + + assert_err!(&result); + } + } +} diff --git a/src/handlers/handlers_tests.rs b/src/handlers/handlers_tests.rs index 9ce97089..51519b95 100644 --- a/src/handlers/handlers_tests.rs +++ b/src/handlers/handlers_tests.rs @@ -22,6 +22,7 @@ mod tests { use crate::models::Route; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; + use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::{ActiveKeybindingBlock, Notification}; use crate::models::servarr_models::KeybindingItem; @@ -61,9 +62,10 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)] + #[case(0, ActiveReadarrBlock::Authors, ActiveSonarrBlock::Series)] #[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)] - #[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)] + #[case(2, ActiveSonarrBlock::Series, ActiveReadarrBlock::Authors)] + #[case(3, ActiveLidarrBlock::Artists, ActiveRadarrBlock::Movies)] fn test_handle_change_tabs( #[case] index: usize, #[case] left_block: T, @@ -226,6 +228,22 @@ mod tests { assert!(!app.data.sonarr_data.prompt_confirm); } + #[rstest] + fn test_handle_prompt_toggle_left_right_readarr(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Authors.into()); + + assert!(!app.data.readarr_data.prompt_confirm); + + handle_prompt_toggle(&mut app, key); + + assert!(app.data.readarr_data.prompt_confirm); + + handle_prompt_toggle(&mut app, key); + + assert!(!app.data.readarr_data.prompt_confirm); + } + #[test] fn test_populate_keymapping_table_global_options() { let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index bf72eb0d..7c6a3402 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,5 +1,6 @@ use lidarr_handlers::LidarrHandler; use radarr_handlers::RadarrHandler; +use readarr_handlers::ReadarrHandler; use sonarr_handlers::SonarrHandler; use crate::app::App; @@ -18,6 +19,7 @@ use crate::models::{HorizontallyScrollableText, Route}; mod keybinding_handler; mod lidarr_handlers; mod radarr_handlers; +mod readarr_handlers; mod sonarr_handlers; #[cfg(test)] @@ -132,6 +134,9 @@ pub fn handle_events(key: Key, app: &mut App<'_>) { Route::Lidarr(active_lidarr_block, context) => { LidarrHandler::new(key, app, active_lidarr_block, context).handle() } + Route::Readarr(active_readarr_block, context) => { + ReadarrHandler::new(key, app, active_readarr_block, context).handle() + } _ => (), } } @@ -197,6 +202,9 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: Key) { Route::Lidarr(_, _) => { app.data.lidarr_data.prompt_confirm = !app.data.lidarr_data.prompt_confirm } + Route::Readarr(_, _) => { + app.data.readarr_data.prompt_confirm = !app.data.readarr_data.prompt_confirm + } _ => (), }, _ => (), diff --git a/src/handlers/readarr_handlers/mod.rs b/src/handlers/readarr_handlers/mod.rs new file mode 100644 index 00000000..9f389546 --- /dev/null +++ b/src/handlers/readarr_handlers/mod.rs @@ -0,0 +1,74 @@ +use super::KeyEventHandler; +use crate::models::Route; +use crate::{ + app::App, event::Key, models::servarr_data::readarr::readarr_data::ActiveReadarrBlock, +}; + +#[cfg(test)] +#[path = "readarr_handler_tests.rs"] +mod readarr_handler_tests; + +pub(super) struct ReadarrHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_readarr_block: ActiveReadarrBlock, + context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for ReadarrHandler<'a, 'b> { + fn accepts(_active_block: ActiveReadarrBlock) -> bool { + true + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveReadarrBlock, + context: Option, + ) -> ReadarrHandler<'a, 'b> { + ReadarrHandler { + key, + app, + active_readarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + true + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) {} + + fn handle_char_key_event(&mut self) {} + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/readarr_handlers/readarr_handler_tests.rs b/src/handlers/readarr_handlers/readarr_handler_tests.rs new file mode 100644 index 00000000..64c4df03 --- /dev/null +++ b/src/handlers/readarr_handlers/readarr_handler_tests.rs @@ -0,0 +1,52 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::handlers::KeyEventHandler; + use crate::handlers::readarr_handlers::ReadarrHandler; + use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + #[rstest] + fn test_readarr_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = ReadarrHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_readarr_handler_is_ready() { + let mut app = App::test_default(); + app.is_loading = true; + + let handler = ReadarrHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::default(), + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_readarr_handler_accepts() { + for readarr_block in ActiveReadarrBlock::iter() { + assert!(ReadarrHandler::accepts(readarr_block)); + } + } +} diff --git a/src/logos.rs b/src/logos.rs index 0205aff5..fdbfd084 100644 --- a/src/logos.rs +++ b/src/logos.rs @@ -25,8 +25,6 @@ pub const BAZARR_LOGO: &str = "⠀⠀⠀⠀⠀⠀⠀⣀⠠⠄⠠⠄⣀⠀⠀⠀ ⠀⠀⠀⠀⠀⠑⢌⠉⠉⠉⠉⠉⠉⡠⠊⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠉⠒⠂⠐⠒⠉⠀⠀⠀⠀⠀⠀⠀ "; -// Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then -#[allow(dead_code)] pub const READARR_LOGO: &str = "⠀⠀⠀⠀⠀⣀⣠⣤⣄⣀⠀⠀⠀⠀⠀ ⠀⠀⢀⡴⠛⠉⠀⠀⠀⠉⠛⢦⡀⠀⠀ ⠀⢠⣯⣄⣀⣐⠻⣿⠟⣂⣀⣠⣽⡄⠀ diff --git a/src/main.rs b/src/main.rs index a40173f4..867b519f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -173,7 +173,7 @@ async fn main() -> Result<()> { match args.command { Some(command) => match command { - Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) => { + Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) | Command::Readarr(_) => { if spinner_disabled { start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await; } else { diff --git a/src/models/mod.rs b/src/models/mod.rs index 3eea7c34..867da8b1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -5,8 +5,10 @@ use crate::app::ServarrConfig; use crate::app::context_clues::ContextClue; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; +use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; use lidarr_models::LidarrSerdeable; use radarr_models::RadarrSerdeable; +use readarr_models::ReadarrSerdeable; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use serde_json::Number; @@ -15,6 +17,7 @@ use sonarr_models::SonarrSerdeable; pub mod lidarr_models; pub mod radarr_models; +pub mod readarr_models; pub mod servarr_data; pub mod servarr_models; pub mod sonarr_models; @@ -32,7 +35,7 @@ mod model_tests; pub enum Route { Radarr(ActiveRadarrBlock, Option), Sonarr(ActiveSonarrBlock, Option), - Readarr, + Readarr(ActiveReadarrBlock, Option), Lidarr(ActiveLidarrBlock, Option), Whisparr, Bazarr, @@ -47,6 +50,7 @@ pub enum Serdeable { Radarr(RadarrSerdeable), Sonarr(SonarrSerdeable), Lidarr(LidarrSerdeable), + Readarr(ReadarrSerdeable), } pub trait Scrollable { diff --git a/src/models/readarr_models.rs b/src/models/readarr_models.rs new file mode 100644 index 00000000..07a99fec --- /dev/null +++ b/src/models/readarr_models.rs @@ -0,0 +1,501 @@ +use super::{ + HorizontallyScrollableText, Serdeable, + servarr_models::{ + DiskSpace, HostConfig, Indexer, IndexerTestResult, MetadataProfile, QualityProfile, + QualityWrapper, RootFolder, SecurityConfig, SystemStatus, Tag, + }, +}; +use crate::models::servarr_models::{IndexerSettings, LogResponse, QueueEvent, Update}; +use crate::serde_enum_from; +use chrono::{DateTime, Utc}; +use clap::ValueEnum; +use derivative::Derivative; +use enum_display_style_derive::EnumDisplayStyle; +use serde::{Deserialize, Serialize}; +use serde_json::{Number, Value}; +use std::fmt::{Display, Formatter}; +use strum::{Display, EnumIter}; + +#[cfg(test)] +#[path = "readarr_models_tests.rs"] +mod readarr_models_tests; + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Author { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub author_name: HorizontallyScrollableText, + pub foreign_author_id: String, + pub status: AuthorStatus, + pub overview: Option, + pub author_type: Option, + pub disambiguation: Option, + pub path: String, + pub root_folder_path: Option, + #[serde(deserialize_with = "super::from_i64")] + pub quality_profile_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub metadata_profile_id: i64, + pub monitored: bool, + pub monitor_new_items: NewItemMonitorType, + pub genres: Vec, + pub tags: Vec, + pub ratings: Option, + pub statistics: Option, +} + +#[derive( + Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, Display, EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum AuthorStatus { + #[default] + Continuing, + Ended, + Deleted, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Ratings { + #[serde(deserialize_with = "super::from_i64")] + pub votes: i64, + #[serde(deserialize_with = "super::from_f64")] + pub value: f64, + pub popularity: Option, +} + +impl Eq for Ratings {} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AuthorStatistics { + #[serde(deserialize_with = "super::from_i64")] + pub book_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub book_file_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub total_book_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub available_book_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub size_on_disk: i64, + #[serde(deserialize_with = "super::from_f64")] + pub percent_of_books: f64, +} + +impl Eq for AuthorStatistics {} + +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + clap::ValueEnum, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum NewItemMonitorType { + #[default] + #[display_style(name = "All Books")] + All, + #[display_style(name = "No New Books")] + None, + #[display_style(name = "New Books")] + New, +} + +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + clap::ValueEnum, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum MonitorType { + #[default] + #[display_style(name = "All Books")] + All, + #[display_style(name = "Future Books")] + Future, + #[display_style(name = "Missing Books")] + Missing, + #[display_style(name = "Existing Books")] + Existing, + #[display_style(name = "First Book")] + First, + #[display_style(name = "Latest Book")] + Latest, + None, + Unknown, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct DownloadRecord { + pub title: String, + pub status: DownloadStatus, + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub book_id: Option, + pub author_id: Option, + #[serde(deserialize_with = "super::from_f64")] + pub size: f64, + #[serde(deserialize_with = "super::from_f64")] + pub sizeleft: f64, + pub output_path: Option, + #[serde(default)] + pub indexer: String, + pub download_client: Option, +} + +impl Eq for DownloadRecord {} + +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum DownloadStatus { + #[default] + Unknown, + Queued, + Paused, + Downloading, + Completed, + Failed, + Warning, + Delay, + #[display_style(name = "Download Client Unavailable")] + DownloadClientUnavailable, + Fallback, +} + +#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct DownloadsResponse { + pub records: Vec, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AddAuthorSearchResult { + pub foreign_author_id: String, + pub author_name: HorizontallyScrollableText, + pub status: AuthorStatus, + #[serde(default)] + pub ended: bool, + pub overview: Option, + pub author_type: Option, + pub disambiguation: Option, + pub genres: Vec, + pub ratings: Option, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Book { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub title: HorizontallyScrollableText, + #[serde(deserialize_with = "super::from_i64")] + pub author_id: i64, + pub foreign_book_id: String, + pub monitored: bool, + #[serde(default)] + pub any_edition_ok: bool, + pub page_count: Option, + pub ratings: Option, + pub release_date: Option>, + pub statistics: Option, + pub grabbed: bool, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BookStatistics { + #[serde(deserialize_with = "super::from_i64")] + pub book_file_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub total_book_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub size_on_disk: i64, + #[serde(deserialize_with = "super::from_f64")] + pub percent_of_books: f64, +} + +impl Eq for BookStatistics {} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Edition { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub book_id: i64, + pub foreign_edition_id: String, + pub monitored: bool, + pub is_ebook: bool, + pub title: String, + pub language: Option, + pub overview: Option, + pub format: Option, + pub publisher: Option, + pub page_count: Option, + pub release_date: Option>, + pub isbn13: Option, + pub asin: Option, + pub ratings: Option, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ReadarrHistoryWrapper { + pub records: Vec, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ReadarrHistoryData { + pub indexer: Option, + pub release_group: Option, + pub nzb_info_url: Option, + pub download_client_name: Option, + pub download_client: Option, + pub age: Option, + pub published_date: Option>, + pub message: Option, + pub reason: Option, + pub dropped_path: Option, + pub imported_path: Option, + pub source_path: Option, + pub path: Option, + pub status_messages: Option, +} + +#[derive( + Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Display, EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum ReadarrHistoryEventType { + #[default] + Unknown, + Grabbed, + #[display_style(name = "Author Folder Imported")] + AuthorFolderImported, + #[display_style(name = "Book Import Incomplete")] + BookImportIncomplete, + #[display_style(name = "Download Ignored")] + DownloadIgnored, + #[display_style(name = "Download Imported")] + DownloadImported, + #[display_style(name = "Download Failed")] + DownloadFailed, + #[display_style(name = "Book File Deleted")] + BookFileDeleted, + #[display_style(name = "Book File Imported")] + BookFileImported, + #[display_style(name = "Book File Renamed")] + BookFileRenamed, + #[display_style(name = "Book File Retagged")] + BookFileRetagged, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ReadarrHistoryItem { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub author_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub book_id: i64, + pub source_title: HorizontallyScrollableText, + #[serde(default)] + pub quality: QualityWrapper, + pub date: DateTime, + pub event_type: ReadarrHistoryEventType, + #[serde(default)] + pub data: ReadarrHistoryData, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ReadarrTask { + pub name: String, + pub task_name: ReadarrTaskName, + #[serde(deserialize_with = "super::from_i64")] + pub interval: i64, + pub last_execution: DateTime, + pub last_duration: String, + pub next_execution: DateTime, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)] +#[serde(rename_all = "PascalCase")] +pub enum ReadarrTaskName { + #[default] + ApplicationUpdateCheck, + Backup, + CheckHealth, + Housekeeping, + ImportListSync, + MessagingCleanup, + RefreshAuthor, + RefreshMonitoredDownloads, + RescanFolders, + RssSync, +} + +impl Display for ReadarrTaskName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let task_name = serde_json::to_string(&self) + .expect("Unable to serialize task name") + .replace('"', ""); + write!(f, "{task_name}") + } +} + +#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +pub struct ReadarrRelease { + pub guid: String, + pub protocol: String, + #[serde(deserialize_with = "super::from_i64")] + pub age: i64, + pub title: HorizontallyScrollableText, + pub author_name: Option, + pub book_title: Option, + pub indexer: String, + #[serde(deserialize_with = "super::from_i64")] + pub indexer_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub size: i64, + pub rejected: bool, + pub rejections: Option>, + pub seeders: Option, + pub leechers: Option, + pub quality: QualityWrapper, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BlocklistItem { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub author_id: i64, + pub book_ids: Option>, + pub source_title: String, + pub quality: QualityWrapper, + pub date: DateTime, + pub protocol: String, + pub indexer: String, + pub message: String, + pub author: Author, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct BlocklistResponse { + pub records: Vec, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BookFile { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub author_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub book_id: i64, + pub path: String, + #[serde(deserialize_with = "super::from_i64")] + pub size: i64, + pub date_added: DateTime, + pub quality: QualityWrapper, + pub media_info: Option, + pub quality_cutoff_not_met: bool, +} + +#[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct MediaInfo { + pub audio_bit_rate: Option, + #[serde(deserialize_with = "super::from_i64")] + pub audio_channels: i64, + pub audio_codec: Option, + pub audio_bits: Option, + pub audio_sample_rate: Option, +} + +impl From for Serdeable { + fn from(value: ReadarrSerdeable) -> Serdeable { + Serdeable::Readarr(value) + } +} + +serde_enum_from!( + ReadarrSerdeable { + AddAuthorSearchResults(Vec), + Author(Author), + Authors(Vec), + BlocklistResponse(BlocklistResponse), + Book(Book), + BookFiles(Vec), + Books(Vec), + DiskSpaces(Vec), + DownloadsResponse(DownloadsResponse), + Editions(Vec), + HostConfig(HostConfig), + Indexers(Vec), + IndexerSettings(IndexerSettings), + IndexerTestResults(Vec), + LogResponse(LogResponse), + MetadataProfiles(Vec), + QualityProfiles(Vec), + QueueEvents(Vec), + ReadarrHistoryItems(Vec), + ReadarrHistoryWrapper(ReadarrHistoryWrapper), + Releases(Vec), + RootFolders(Vec), + SecurityConfig(SecurityConfig), + SystemStatus(SystemStatus), + Tag(Tag), + Tags(Vec), + Tasks(Vec), + Updates(Vec), + Value(Value), + } +); diff --git a/src/models/readarr_models_tests.rs b/src/models/readarr_models_tests.rs new file mode 100644 index 00000000..fe163400 --- /dev/null +++ b/src/models/readarr_models_tests.rs @@ -0,0 +1,1022 @@ +#[cfg(test)] +mod tests { + use chrono::Utc; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::json; + + use crate::models::readarr_models::{ + AddAuthorSearchResult, BlocklistItem, BlocklistResponse, Book, BookFile, DownloadRecord, + DownloadStatus, DownloadsResponse, Edition, MediaInfo, MonitorType, NewItemMonitorType, + ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrHistoryWrapper, ReadarrRelease, + ReadarrTask, ReadarrTaskName, + }; + use crate::models::servarr_models::{ + DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse, + MetadataProfile, QualityProfile, QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag, + Update, + }; + use crate::models::{ + Serdeable, + readarr_models::{Author, AuthorStatistics, AuthorStatus, Ratings, ReadarrSerdeable}, + }; + + #[test] + fn test_author_status_default() { + assert_eq!(AuthorStatus::default(), AuthorStatus::Continuing); + } + + #[test] + fn test_new_item_monitor_type_display() { + assert_str_eq!(NewItemMonitorType::All.to_string(), "all"); + assert_str_eq!(NewItemMonitorType::None.to_string(), "none"); + assert_str_eq!(NewItemMonitorType::New.to_string(), "new"); + } + + #[test] + fn test_new_item_monitor_type_to_display_str() { + assert_str_eq!(NewItemMonitorType::All.to_display_str(), "All Books"); + assert_str_eq!(NewItemMonitorType::None.to_display_str(), "No New Books"); + assert_str_eq!(NewItemMonitorType::New.to_display_str(), "New Books"); + } + + #[test] + fn test_monitor_type_display() { + assert_str_eq!(MonitorType::All.to_string(), "all"); + assert_str_eq!(MonitorType::Future.to_string(), "future"); + assert_str_eq!(MonitorType::Missing.to_string(), "missing"); + assert_str_eq!(MonitorType::Existing.to_string(), "existing"); + assert_str_eq!(MonitorType::First.to_string(), "first"); + assert_str_eq!(MonitorType::Latest.to_string(), "latest"); + assert_str_eq!(MonitorType::None.to_string(), "none"); + assert_str_eq!(MonitorType::Unknown.to_string(), "unknown"); + } + + #[test] + fn test_monitor_type_to_display_str() { + assert_str_eq!(MonitorType::All.to_display_str(), "All Books"); + assert_str_eq!(MonitorType::Future.to_display_str(), "Future Books"); + assert_str_eq!(MonitorType::Missing.to_display_str(), "Missing Books"); + assert_str_eq!(MonitorType::Existing.to_display_str(), "Existing Books"); + assert_str_eq!(MonitorType::First.to_display_str(), "First Book"); + assert_str_eq!(MonitorType::Latest.to_display_str(), "Latest Book"); + assert_str_eq!(MonitorType::None.to_display_str(), "None"); + assert_str_eq!(MonitorType::Unknown.to_display_str(), "Unknown"); + } + + #[test] + fn test_readarr_serdeable_from() { + let readarr_serdeable = ReadarrSerdeable::Value(json!({})); + + let serdeable: Serdeable = Serdeable::from(readarr_serdeable.clone()); + + assert_eq!(serdeable, Serdeable::Readarr(readarr_serdeable)); + } + + #[test] + fn test_readarr_serdeable_from_unit() { + let readarr_serdeable = ReadarrSerdeable::from(()); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Value(json!({}))); + } + + #[test] + fn test_readarr_serdeable_from_value() { + let value = json!({"test": "test"}); + + let readarr_serdeable: ReadarrSerdeable = value.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Value(value)); + } + + #[test] + fn test_readarr_serdeable_from_authors() { + let authors = vec![Author { + id: 1, + ..Author::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = authors.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Authors(authors)); + } + + #[test] + fn test_author_deserialization() { + let author_json = json!({ + "id": 1, + "authorName": "Test Author", + "foreignAuthorId": "test-foreign-id", + "status": "continuing", + "overview": "Test overview", + "authorType": "Person", + "disambiguation": "American Novelist", + "path": "/nfs/books/Test Author", + "rootFolderPath": "/nfs/books/", + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": ["Fantasy", "Fiction"], + "tags": [1, 2], + "ratings": { + "votes": 100, + "value": 4.5, + "popularity": 450.0 + }, + "statistics": { + "bookCount": 14, + "bookFileCount": 5, + "totalBookCount": 14, + "availableBookCount": 5, + "sizeOnDisk": 51783488, + "percentOfBooks": 35.71 + } + }); + + let author: Author = serde_json::from_value(author_json).unwrap(); + + assert_eq!(author.id, 1); + assert_str_eq!(author.author_name.text, "Test Author"); + assert_str_eq!(author.foreign_author_id, "test-foreign-id"); + assert_eq!(author.status, AuthorStatus::Continuing); + assert_some_eq_x!(&author.overview, "Test overview"); + assert_some_eq_x!(&author.author_type, "Person"); + assert_some_eq_x!(&author.disambiguation, "American Novelist"); + assert_str_eq!(author.path, "/nfs/books/Test Author"); + assert_some_eq_x!(&author.root_folder_path, "/nfs/books/"); + assert_eq!(author.quality_profile_id, 1); + assert_eq!(author.metadata_profile_id, 1); + assert!(author.monitored); + assert_eq!(author.monitor_new_items, NewItemMonitorType::All); + assert_eq!(author.genres, vec!["Fantasy", "Fiction"]); + assert_eq!(author.tags.len(), 2); + assert_some!(&author.ratings); + assert_some!(&author.statistics); + + let ratings = author.ratings.unwrap(); + assert_eq!(ratings.votes, 100); + assert_eq!(ratings.value, 4.5); + assert_some_eq_x!(ratings.popularity, 450.0); + + let stats = author.statistics.unwrap(); + assert_eq!(stats.book_count, 14); + assert_eq!(stats.book_file_count, 5); + assert_eq!(stats.total_book_count, 14); + assert_eq!(stats.available_book_count, 5); + assert_eq!(stats.size_on_disk, 51783488); + assert_eq!(stats.percent_of_books, 35.71); + } + + #[test] + fn test_author_status_deserialization() { + assert_eq!( + serde_json::from_str::("\"continuing\"").unwrap(), + AuthorStatus::Continuing + ); + assert_eq!( + serde_json::from_str::("\"ended\"").unwrap(), + AuthorStatus::Ended + ); + assert_eq!( + serde_json::from_str::("\"deleted\"").unwrap(), + AuthorStatus::Deleted + ); + } + + #[test] + fn test_ratings_equality() { + let ratings1 = Ratings { + votes: 100, + value: 4.5, + popularity: Some(450.0), + }; + let ratings2 = Ratings { + votes: 100, + value: 4.5, + popularity: Some(450.0), + }; + let ratings3 = Ratings { + votes: 50, + value: 3.0, + popularity: None, + }; + + assert_eq!(ratings1, ratings2); + assert_ne!(ratings1, ratings3); + } + + #[test] + fn test_author_statistics_equality() { + let stats1 = AuthorStatistics { + book_count: 14, + book_file_count: 5, + total_book_count: 14, + available_book_count: 5, + size_on_disk: 51783488, + percent_of_books: 35.71, + }; + let stats2 = AuthorStatistics { + book_count: 14, + book_file_count: 5, + total_book_count: 14, + available_book_count: 5, + size_on_disk: 51783488, + percent_of_books: 35.71, + }; + let stats3 = AuthorStatistics::default(); + + assert_eq!(stats1, stats2); + assert_ne!(stats1, stats3); + } + + #[test] + fn test_author_with_optional_fields_none() { + let author_json = json!({ + "id": 1, + "authorName": "Test Author", + "foreignAuthorId": "", + "status": "continuing", + "path": "", + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": false, + "monitorNewItems": "all", + "genres": [], + "tags": [] + }); + + let author: Author = serde_json::from_value(author_json).unwrap(); + + assert_none!(&author.overview); + assert_none!(&author.author_type); + assert_none!(&author.disambiguation); + assert_none!(&author.root_folder_path); + assert_eq!(author.monitor_new_items, NewItemMonitorType::All); + assert_none!(&author.ratings); + assert_none!(&author.statistics); + } + + #[test] + fn test_readarr_serdeable_from_author() { + let author = Author { + id: 1, + ..Author::default() + }; + + let readarr_serdeable: ReadarrSerdeable = author.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Author(author)); + } + + #[test] + fn test_readarr_serdeable_from_blocklist_response() { + let blocklist_response = BlocklistResponse { + records: vec![BlocklistItem { + id: 1, + ..BlocklistItem::default() + }], + }; + + let readarr_serdeable: ReadarrSerdeable = blocklist_response.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::BlocklistResponse(blocklist_response) + ); + } + + #[test] + fn test_readarr_serdeable_from_disk_spaces() { + let disk_spaces = vec![DiskSpace { + path: Some("/path".to_owned()), + free_space: 1, + total_space: 1, + }]; + + let readarr_serdeable: ReadarrSerdeable = disk_spaces.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::DiskSpaces(disk_spaces)); + } + + #[test] + fn test_readarr_serdeable_from_downloads_response() { + let downloads_response = DownloadsResponse { + records: vec![DownloadRecord { + id: 1, + ..DownloadRecord::default() + }], + }; + + let readarr_serdeable: ReadarrSerdeable = downloads_response.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::DownloadsResponse(downloads_response) + ); + } + + #[test] + fn test_readarr_serdeable_from_readarr_history_wrapper() { + let history_wrapper = ReadarrHistoryWrapper { + records: vec![ReadarrHistoryItem { + id: 1, + ..ReadarrHistoryItem::default() + }], + }; + + let readarr_serdeable: ReadarrSerdeable = history_wrapper.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::ReadarrHistoryWrapper(history_wrapper) + ); + } + + #[test] + fn test_readarr_serdeable_from_readarr_history_items() { + let history_items = vec![ReadarrHistoryItem { + id: 1, + ..ReadarrHistoryItem::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = history_items.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::ReadarrHistoryItems(history_items) + ); + } + + #[test] + fn test_readarr_serdeable_from_indexers() { + let indexers = vec![Indexer { + id: 1, + ..Indexer::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = indexers.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Indexers(indexers)); + } + + #[test] + fn test_readarr_serdeable_from_indexer_settings() { + let indexer_settings = IndexerSettings { + id: 1, + ..IndexerSettings::default() + }; + + let readarr_serdeable: ReadarrSerdeable = indexer_settings.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::IndexerSettings(indexer_settings) + ); + } + + #[test] + fn test_readarr_serdeable_from_indexer_test_results() { + let indexer_test_results = vec![IndexerTestResult { + id: 1, + ..IndexerTestResult::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = indexer_test_results.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::IndexerTestResults(indexer_test_results) + ); + } + + #[test] + fn test_readarr_serdeable_from_log_response() { + let log_response = LogResponse { + records: vec![Log { + level: "info".to_owned(), + ..Log::default() + }], + }; + + let readarr_serdeable: ReadarrSerdeable = log_response.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::LogResponse(log_response) + ); + } + + #[test] + fn test_readarr_serdeable_from_metadata_profiles() { + let metadata_profiles = vec![MetadataProfile { + id: 1, + name: "Standard".to_owned(), + }]; + + let readarr_serdeable: ReadarrSerdeable = metadata_profiles.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::MetadataProfiles(metadata_profiles) + ); + } + + #[test] + fn test_readarr_serdeable_from_host_config() { + let host_config = HostConfig { + port: 8787, + ..HostConfig::default() + }; + + let readarr_serdeable: ReadarrSerdeable = host_config.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::HostConfig(host_config)); + } + + #[test] + fn test_readarr_serdeable_from_quality_profiles() { + let quality_profiles = vec![QualityProfile { + id: 1, + name: "Any".to_owned(), + }]; + + let readarr_serdeable: ReadarrSerdeable = quality_profiles.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::QualityProfiles(quality_profiles) + ); + } + + #[test] + fn test_readarr_serdeable_from_queue_events() { + let queue_events = vec![QueueEvent { + trigger: "test".to_owned(), + ..QueueEvent::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = queue_events.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::QueueEvents(queue_events) + ); + } + + #[test] + fn test_readarr_serdeable_from_root_folders() { + let root_folders = vec![RootFolder { + id: 1, + path: "/nfs/books".to_owned(), + accessible: true, + free_space: 1000000, + unmapped_folders: None, + }]; + + let readarr_serdeable: ReadarrSerdeable = root_folders.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::RootFolders(root_folders) + ); + } + + #[test] + fn test_readarr_serdeable_from_releases() { + let releases = vec![ReadarrRelease { + guid: "test".to_owned(), + ..ReadarrRelease::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = releases.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Releases(releases)); + } + + #[test] + fn test_readarr_serdeable_from_security_config() { + let security_config = SecurityConfig { + api_key: "test-key".to_owned(), + ..SecurityConfig::default() + }; + + let readarr_serdeable: ReadarrSerdeable = security_config.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::SecurityConfig(security_config) + ); + } + + #[test] + fn test_readarr_serdeable_from_system_status() { + let system_status = SystemStatus { + version: "1.0.0".to_owned(), + start_time: Utc::now(), + }; + + let readarr_serdeable: ReadarrSerdeable = system_status.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::SystemStatus(system_status) + ); + } + + #[test] + fn test_readarr_serdeable_from_tags() { + let tags = vec![Tag { + id: 1, + label: "fantasy".to_owned(), + }]; + + let readarr_serdeable: ReadarrSerdeable = tags.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Tags(tags)); + } + + #[test] + fn test_readarr_serdeable_from_add_author_search_results() { + let search_results = vec![AddAuthorSearchResult { + foreign_author_id: "test-id".to_owned(), + ..AddAuthorSearchResult::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = search_results.clone().into(); + + assert_eq!( + readarr_serdeable, + ReadarrSerdeable::AddAuthorSearchResults(search_results) + ); + } + + #[test] + fn test_readarr_serdeable_from_books() { + let books = vec![Book { + id: 1, + ..Book::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = books.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Books(books)); + } + + #[test] + fn test_readarr_serdeable_from_book() { + let book = Book { + id: 1, + ..Book::default() + }; + + let readarr_serdeable: ReadarrSerdeable = book.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Book(book)); + } + + #[test] + fn test_readarr_serdeable_from_editions() { + let editions = vec![Edition { + id: 1, + ..Edition::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = editions.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Editions(editions)); + } + + #[test] + fn test_readarr_serdeable_from_book_files() { + let book_files = vec![BookFile { + id: 1, + media_info: Some(MediaInfo { + audio_channels: 2, + ..MediaInfo::default() + }), + ..BookFile::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = book_files.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::BookFiles(book_files)); + } + + #[test] + fn test_readarr_serdeable_from_tasks() { + let tasks = vec![ReadarrTask { + name: "test".to_owned(), + ..ReadarrTask::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = tasks.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Tasks(tasks)); + } + + #[test] + fn test_readarr_serdeable_from_updates() { + let updates = vec![Update { + version: "test".to_owned(), + ..Update::default() + }]; + + let readarr_serdeable: ReadarrSerdeable = updates.clone().into(); + + assert_eq!(readarr_serdeable, ReadarrSerdeable::Updates(updates)); + } + + #[test] + fn test_author_status_display() { + assert_str_eq!(AuthorStatus::Continuing.to_string(), "continuing"); + assert_str_eq!(AuthorStatus::Ended.to_string(), "ended"); + assert_str_eq!(AuthorStatus::Deleted.to_string(), "deleted"); + } + + #[test] + fn test_author_status_to_display_str() { + assert_str_eq!(AuthorStatus::Continuing.to_display_str(), "Continuing"); + assert_str_eq!(AuthorStatus::Ended.to_display_str(), "Ended"); + assert_str_eq!(AuthorStatus::Deleted.to_display_str(), "Deleted"); + } + + #[test] + fn test_download_status_display() { + assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown"); + assert_str_eq!(DownloadStatus::Queued.to_string(), "queued"); + assert_str_eq!(DownloadStatus::Paused.to_string(), "paused"); + assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading"); + assert_str_eq!(DownloadStatus::Completed.to_string(), "completed"); + assert_str_eq!(DownloadStatus::Failed.to_string(), "failed"); + assert_str_eq!(DownloadStatus::Warning.to_string(), "warning"); + assert_str_eq!(DownloadStatus::Delay.to_string(), "delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_string(), + "downloadClientUnavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback"); + } + + #[test] + fn test_download_status_to_display_str() { + assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued"); + assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused"); + assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading"); + assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed"); + assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed"); + assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning"); + assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_display_str(), + "Download Client Unavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback"); + } + + #[test] + fn test_readarr_history_event_type_display() { + assert_str_eq!(ReadarrHistoryEventType::Unknown.to_string(), "unknown"); + assert_str_eq!(ReadarrHistoryEventType::Grabbed.to_string(), "grabbed"); + assert_str_eq!( + ReadarrHistoryEventType::AuthorFolderImported.to_string(), + "authorFolderImported" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookImportIncomplete.to_string(), + "bookImportIncomplete" + ); + assert_str_eq!( + ReadarrHistoryEventType::DownloadIgnored.to_string(), + "downloadIgnored" + ); + assert_str_eq!( + ReadarrHistoryEventType::DownloadImported.to_string(), + "downloadImported" + ); + assert_str_eq!( + ReadarrHistoryEventType::DownloadFailed.to_string(), + "downloadFailed" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookFileDeleted.to_string(), + "bookFileDeleted" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookFileImported.to_string(), + "bookFileImported" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookFileRenamed.to_string(), + "bookFileRenamed" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookFileRetagged.to_string(), + "bookFileRetagged" + ); + } + + #[test] + fn test_readarr_history_event_type_to_display_str() { + assert_str_eq!(ReadarrHistoryEventType::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(ReadarrHistoryEventType::Grabbed.to_display_str(), "Grabbed"); + assert_str_eq!( + ReadarrHistoryEventType::AuthorFolderImported.to_display_str(), + "Author Folder Imported" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookImportIncomplete.to_display_str(), + "Book Import Incomplete" + ); + assert_str_eq!( + ReadarrHistoryEventType::DownloadIgnored.to_display_str(), + "Download Ignored" + ); + assert_str_eq!( + ReadarrHistoryEventType::DownloadImported.to_display_str(), + "Download Imported" + ); + assert_str_eq!( + ReadarrHistoryEventType::DownloadFailed.to_display_str(), + "Download Failed" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookFileDeleted.to_display_str(), + "Book File Deleted" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookFileImported.to_display_str(), + "Book File Imported" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookFileRenamed.to_display_str(), + "Book File Renamed" + ); + assert_str_eq!( + ReadarrHistoryEventType::BookFileRetagged.to_display_str(), + "Book File Retagged" + ); + } + + #[test] + fn test_readarr_task_name_display() { + assert_str_eq!( + ReadarrTaskName::ApplicationUpdateCheck.to_string(), + "ApplicationUpdateCheck" + ); + assert_str_eq!(ReadarrTaskName::Backup.to_string(), "Backup"); + assert_str_eq!(ReadarrTaskName::CheckHealth.to_string(), "CheckHealth"); + assert_str_eq!(ReadarrTaskName::Housekeeping.to_string(), "Housekeeping"); + assert_str_eq!( + ReadarrTaskName::ImportListSync.to_string(), + "ImportListSync" + ); + assert_str_eq!( + ReadarrTaskName::MessagingCleanup.to_string(), + "MessagingCleanup" + ); + assert_str_eq!(ReadarrTaskName::RefreshAuthor.to_string(), "RefreshAuthor"); + assert_str_eq!( + ReadarrTaskName::RefreshMonitoredDownloads.to_string(), + "RefreshMonitoredDownloads" + ); + assert_str_eq!(ReadarrTaskName::RescanFolders.to_string(), "RescanFolders"); + assert_str_eq!(ReadarrTaskName::RssSync.to_string(), "RssSync"); + } + + #[test] + fn test_book_deserialization() { + let book_json = json!({ + "id": 1, + "title": "The Name of the Wind", + "authorId": 3, + "foreignBookId": "test-foreign-book-id", + "monitored": true, + "anyEditionOk": true, + "pageCount": 662, + "releaseDate": "2007-03-27T00:00:00Z", + "grabbed": false, + "ratings": { + "votes": 100, + "value": 4.5, + "popularity": 450.0 + }, + "statistics": { + "bookFileCount": 1, + "totalBookCount": 1, + "sizeOnDisk": 51783488, + "percentOfBooks": 100.0 + } + }); + + let book: Book = serde_json::from_value(book_json).unwrap(); + + assert_eq!(book.id, 1); + assert_str_eq!(book.title.text, "The Name of the Wind"); + assert_eq!(book.author_id, 3); + assert_str_eq!(book.foreign_book_id, "test-foreign-book-id"); + assert!(book.monitored); + assert!(book.any_edition_ok); + assert_some_eq_x!(book.page_count, 662); + assert_some!(&book.release_date); + assert!(!book.grabbed); + assert_some!(&book.ratings); + + let stats = book.statistics.unwrap(); + assert_eq!(stats.book_file_count, 1); + assert_eq!(stats.total_book_count, 1); + assert_eq!(stats.size_on_disk, 51783488); + assert_eq!(stats.percent_of_books, 100.0); + } + + #[test] + fn test_book_with_optional_fields_none() { + let book_json = json!({ + "id": 1, + "title": "The Name of the Wind", + "authorId": 3, + "foreignBookId": "", + "monitored": false, + "grabbed": false + }); + + let book: Book = serde_json::from_value(book_json).unwrap(); + + assert!(!book.any_edition_ok); + assert_none!(&book.page_count); + assert_none!(&book.release_date); + assert_none!(&book.ratings); + assert_none!(&book.statistics); + } + + #[test] + fn test_edition_deserialization() { + let edition_json = json!({ + "id": 106, + "bookId": 1, + "foreignEditionId": "test-foreign-edition-id", + "monitored": true, + "isEbook": false, + "title": "The Name of the Wind", + "language": "eng", + "overview": "Test overview", + "format": "Hardcover", + "publisher": "DAW Books", + "pageCount": 662, + "releaseDate": "2007-03-27T00:00:00Z", + "isbn13": "9780756404079", + "asin": "0756404070", + "ratings": { + "votes": 100, + "value": 4.5, + "popularity": 450.0 + } + }); + + let edition: Edition = serde_json::from_value(edition_json).unwrap(); + + assert_eq!(edition.id, 106); + assert_eq!(edition.book_id, 1); + assert_str_eq!(edition.foreign_edition_id, "test-foreign-edition-id"); + assert!(edition.monitored); + assert!(!edition.is_ebook); + assert_str_eq!(edition.title, "The Name of the Wind"); + assert_some_eq_x!(&edition.language, "eng"); + assert_some_eq_x!(&edition.overview, "Test overview"); + assert_some_eq_x!(&edition.format, "Hardcover"); + assert_some_eq_x!(&edition.publisher, "DAW Books"); + assert_some_eq_x!(edition.page_count, 662); + assert_some!(&edition.release_date); + assert_some_eq_x!(&edition.isbn13, "9780756404079"); + assert_some_eq_x!(&edition.asin, "0756404070"); + assert_some!(&edition.ratings); + } + + #[test] + fn test_edition_with_optional_fields_none() { + let edition_json = json!({ + "id": 106, + "bookId": 1, + "foreignEditionId": "", + "monitored": false, + "isEbook": true, + "title": "" + }); + + let edition: Edition = serde_json::from_value(edition_json).unwrap(); + + assert_none!(&edition.language); + assert_none!(&edition.overview); + assert_none!(&edition.format); + assert_none!(&edition.publisher); + assert_none!(&edition.page_count); + assert_none!(&edition.release_date); + assert_none!(&edition.isbn13); + assert_none!(&edition.asin); + assert_none!(&edition.ratings); + } + + #[test] + fn test_media_info_deserialization() { + let media_info_json = json!({ + "audioChannels": 2, + "audioBitRate": "99 kbps", + "audioCodec": "AAC", + "audioBits": "", + "audioSampleRate": "44.1kHz" + }); + + let media_info: MediaInfo = serde_json::from_value(media_info_json).unwrap(); + + assert_eq!(media_info.audio_channels, 2); + assert_some_eq_x!(&media_info.audio_bit_rate, "99 kbps"); + assert_some_eq_x!(&media_info.audio_codec, "AAC"); + assert_some_eq_x!(&media_info.audio_bits, ""); + assert_some_eq_x!(&media_info.audio_sample_rate, "44.1kHz"); + } + + #[test] + fn test_download_record_deserialization() { + let download_record_json = json!({ + "title": "Test Download", + "status": "downloading", + "id": 1, + "bookId": 2, + "authorId": 3, + "size": 3543348019.0, + "sizeleft": 1771674009.5, + "outputPath": "/nfs/books/Test Author", + "downloadClient": "transmission" + }); + + let download_record: DownloadRecord = serde_json::from_value(download_record_json).unwrap(); + + assert_str_eq!(download_record.title, "Test Download"); + assert_eq!(download_record.status, DownloadStatus::Downloading); + assert_eq!(download_record.id, 1); + assert_some!(&download_record.book_id); + assert_some!(&download_record.author_id); + assert_eq!(download_record.size, 3543348019.0); + assert_eq!(download_record.sizeleft, 1771674009.5); + assert_some!(&download_record.output_path); + assert!(download_record.indexer.is_empty()); + assert_some_eq_x!(&download_record.download_client, "transmission"); + } + + #[test] + fn test_add_author_search_result_deserialization() { + let search_result_json = json!({ + "foreignAuthorId": "test-foreign-id", + "authorName": "Test Author", + "status": "continuing", + "ended": false, + "overview": "Test overview", + "authorType": "Person", + "disambiguation": "American Novelist", + "genres": ["Fantasy", "Fiction"], + "ratings": { + "votes": 100, + "value": 4.5, + "popularity": 450.0 + } + }); + + let search_result: AddAuthorSearchResult = serde_json::from_value(search_result_json).unwrap(); + + assert_str_eq!(search_result.foreign_author_id, "test-foreign-id"); + assert_str_eq!(search_result.author_name.text, "Test Author"); + assert_eq!(search_result.status, AuthorStatus::Continuing); + assert!(!search_result.ended); + assert_some_eq_x!(&search_result.overview, "Test overview"); + assert_some_eq_x!(&search_result.author_type, "Person"); + assert_some_eq_x!(&search_result.disambiguation, "American Novelist"); + assert_eq!(search_result.genres, vec!["Fantasy", "Fiction"]); + assert_some!(&search_result.ratings); + + let ratings = search_result.ratings.unwrap(); + assert_eq!(ratings.votes, 100); + assert_eq!(ratings.value, 4.5); + } + + #[test] + fn test_add_author_search_result_with_optional_fields_none() { + let search_result_json = json!({ + "foreignAuthorId": "test-foreign-id", + "authorName": "Test Author", + "status": "ended", + "genres": [] + }); + + let search_result: AddAuthorSearchResult = serde_json::from_value(search_result_json).unwrap(); + + assert_str_eq!(search_result.foreign_author_id, "test-foreign-id"); + assert_str_eq!(search_result.author_name.text, "Test Author"); + assert_eq!(search_result.status, AuthorStatus::Ended); + assert!(!search_result.ended); + assert_none!(&search_result.overview); + assert_none!(&search_result.author_type); + assert_none!(&search_result.disambiguation); + assert!(search_result.genres.is_empty()); + assert_none!(&search_result.ratings); + } +} diff --git a/src/models/servarr_data/mod.rs b/src/models/servarr_data/mod.rs index c0583806..c329e662 100644 --- a/src/models/servarr_data/mod.rs +++ b/src/models/servarr_data/mod.rs @@ -3,6 +3,7 @@ use crate::models::Route; pub mod lidarr; pub mod modals; pub mod radarr; +pub mod readarr; pub mod sonarr; #[cfg(test)] diff --git a/src/models/servarr_data/readarr/mod.rs b/src/models/servarr_data/readarr/mod.rs new file mode 100644 index 00000000..84c3cdfd --- /dev/null +++ b/src/models/servarr_data/readarr/mod.rs @@ -0,0 +1,2 @@ +pub mod modals; +pub mod readarr_data; diff --git a/src/models/servarr_data/readarr/modals.rs b/src/models/servarr_data/readarr/modals.rs new file mode 100644 index 00000000..30b0a817 --- /dev/null +++ b/src/models/servarr_data/readarr/modals.rs @@ -0,0 +1,271 @@ +use super::readarr_data::{ActiveReadarrBlock, ReadarrData}; +use crate::app::readarr::readarr_context_clues::{ + BOOK_DETAILS_CONTEXT_CLUES, BOOK_HISTORY_CONTEXT_CLUES, MANUAL_BOOK_SEARCH_CONTEXT_CLUES, +}; +use crate::models::readarr_models::{BookFile, Edition, ReadarrHistoryItem, ReadarrRelease}; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_models::Indexer; +use crate::models::stateful_table::StatefulTable; +use crate::models::{ + HorizontallyScrollableText, ScrollableText, TabRoute, TabState, + readarr_models::{MonitorType, NewItemMonitorType}, + servarr_models::RootFolder, + stateful_list::StatefulList, +}; +use strum::IntoEnumIterator; + +#[cfg(test)] +#[path = "modals_tests.rs"] +mod modals_tests; + +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct AddAuthorModal { + pub root_folder_list: StatefulList, + pub monitor_list: StatefulList, + pub monitor_new_items_list: StatefulList, + pub quality_profile_list: StatefulList, + pub metadata_profile_list: StatefulList, + pub tags: HorizontallyScrollableText, +} + +impl From<&ReadarrData<'_>> for AddAuthorModal { + fn from(readarr_data: &ReadarrData<'_>) -> AddAuthorModal { + let mut add_author_modal = AddAuthorModal::default(); + add_author_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_author_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_author_modal + .quality_profile_list + .set_items(readarr_data.sorted_quality_profile_names()); + add_author_modal + .metadata_profile_list + .set_items(readarr_data.sorted_metadata_profile_names()); + add_author_modal + .root_folder_list + .set_items(readarr_data.root_folders.items.to_vec()); + + add_author_modal + } +} + +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct EditAuthorModal { + pub monitor_list: StatefulList, + pub quality_profile_list: StatefulList, + pub metadata_profile_list: StatefulList, + pub monitored: Option, + pub path: HorizontallyScrollableText, + pub tags: HorizontallyScrollableText, +} + +impl From<&ReadarrData<'_>> for EditAuthorModal { + fn from(readarr_data: &ReadarrData<'_>) -> EditAuthorModal { + let mut edit_author_modal = EditAuthorModal::default(); + let author = readarr_data.authors.current_selection(); + + edit_author_modal + .monitor_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + edit_author_modal.path = author.path.clone().into(); + edit_author_modal.tags = readarr_data.tag_ids_to_display(&author.tags).into(); + edit_author_modal.monitored = Some(author.monitored); + + let monitor_index = edit_author_modal + .monitor_list + .items + .iter() + .position(|m| *m == author.monitor_new_items); + edit_author_modal.monitor_list.state.select(monitor_index); + + edit_author_modal + .quality_profile_list + .set_items(readarr_data.sorted_quality_profile_names()); + let quality_profile_name = readarr_data + .quality_profile_map + .get_by_left(&author.quality_profile_id) + .unwrap(); + let quality_profile_index = edit_author_modal + .quality_profile_list + .items + .iter() + .position(|profile| profile == quality_profile_name); + edit_author_modal + .quality_profile_list + .state + .select(quality_profile_index); + + edit_author_modal + .metadata_profile_list + .set_items(readarr_data.sorted_metadata_profile_names()); + let metadata_profile_name = readarr_data + .metadata_profile_map + .get_by_left(&author.metadata_profile_id) + .unwrap(); + let metadata_profile_index = edit_author_modal + .metadata_profile_list + .items + .iter() + .position(|profile| profile == metadata_profile_name); + edit_author_modal + .metadata_profile_list + .state + .select(metadata_profile_index); + + edit_author_modal + } +} + +impl From<&ReadarrData<'_>> for EditIndexerModal { + fn from(readarr_data: &ReadarrData<'_>) -> EditIndexerModal { + let mut edit_indexer_modal = EditIndexerModal::default(); + let Indexer { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + tags, + fields, + priority, + .. + } = readarr_data.indexers.current_selection(); + let seed_ratio_field_option = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| { + field.name.as_ref().expect("indexer field name must exist") == "seedCriteria.seedRatio" + }); + let seed_ratio_value_option = if let Some(seed_ratio_field) = seed_ratio_field_option { + seed_ratio_field.value.clone() + } else { + None + }; + + edit_indexer_modal.name = name.clone().expect("indexer name must exist").into(); + edit_indexer_modal.enable_rss = Some(*enable_rss); + edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search); + edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search); + edit_indexer_modal.priority = *priority; + edit_indexer_modal.url = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| field.name.as_ref().expect("indexer field name must exist") == "baseUrl") + .expect("baseUrl field must exist") + .value + .clone() + .expect("baseUrl field value must exist") + .as_str() + .expect("baseUrl field value must be a string") + .into(); + edit_indexer_modal.api_key = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| field.name.as_ref().expect("indexer field name must exist") == "apiKey") + .expect("apiKey field must exist") + .value + .clone() + .expect("apiKey field value must exist") + .as_str() + .expect("apiKey field value must be a string") + .into(); + + if let Some(seed_ratio_value) = seed_ratio_value_option { + edit_indexer_modal.seed_ratio = seed_ratio_value + .as_f64() + .expect("Seed ratio value must be a valid f64") + .to_string() + .into(); + } + + edit_indexer_modal.tags = readarr_data.tag_ids_to_display(tags).into(); + + edit_indexer_modal + } +} + +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct AddReadarrRootFolderModal { + pub name: HorizontallyScrollableText, + pub path: HorizontallyScrollableText, + pub monitor_list: StatefulList, + pub monitor_new_items_list: StatefulList, + pub quality_profile_list: StatefulList, + pub metadata_profile_list: StatefulList, + pub tags: HorizontallyScrollableText, +} + +impl From<&ReadarrData<'_>> for AddReadarrRootFolderModal { + fn from(readarr_data: &ReadarrData<'_>) -> AddReadarrRootFolderModal { + let mut add_root_folder_modal = AddReadarrRootFolderModal::default(); + add_root_folder_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_root_folder_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_root_folder_modal + .quality_profile_list + .set_items(readarr_data.sorted_quality_profile_names()); + add_root_folder_modal + .metadata_profile_list + .set_items(readarr_data.sorted_metadata_profile_names()); + + add_root_folder_modal + } +} + +#[cfg_attr(test, derive(Debug))] +pub struct BookDetailsModal { + pub editions: StatefulTable, + pub book_files: StatefulTable, + pub edition_details_modal: Option, + pub book_history: StatefulTable, + pub book_releases: StatefulTable, + pub book_details_tabs: TabState, +} + +impl Default for BookDetailsModal { + fn default() -> BookDetailsModal { + BookDetailsModal { + editions: StatefulTable::default(), + edition_details_modal: None, + book_files: StatefulTable::default(), + book_releases: StatefulTable::default(), + book_history: StatefulTable::default(), + book_details_tabs: TabState::new(vec![ + TabRoute { + title: "Editions".to_string(), + route: ActiveReadarrBlock::BookDetails.into(), + contextual_help: Some(&BOOK_DETAILS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "History".to_string(), + route: ActiveReadarrBlock::BookHistory.into(), + contextual_help: Some(&BOOK_HISTORY_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Manual Search".to_string(), + route: ActiveReadarrBlock::ManualBookSearch.into(), + contextual_help: Some(&MANUAL_BOOK_SEARCH_CONTEXT_CLUES), + config: None, + }, + ]), + } + } +} + +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct EditionDetailsModal { + pub edition_details: ScrollableText, +} diff --git a/src/models/servarr_data/readarr/modals_tests.rs b/src/models/servarr_data/readarr/modals_tests.rs new file mode 100644 index 00000000..3d7f7c7a --- /dev/null +++ b/src/models/servarr_data/readarr/modals_tests.rs @@ -0,0 +1,322 @@ +#[cfg(test)] +mod tests { + use crate::app::readarr::readarr_context_clues::{ + BOOK_DETAILS_CONTEXT_CLUES, BOOK_HISTORY_CONTEXT_CLUES, MANUAL_BOOK_SEARCH_CONTEXT_CLUES, + }; + use crate::models::readarr_models::{Author, MonitorType, NewItemMonitorType}; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_data::readarr::modals::{ + AddAuthorModal, AddReadarrRootFolderModal, BookDetailsModal, EditAuthorModal, + EditionDetailsModal, + }; + use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData}; + use crate::models::servarr_models::{Indexer, IndexerField, RootFolder}; + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use serde_json::{Number, Value}; + + #[test] + fn test_add_author_modal_from_readarr_data() { + let mut readarr_data = ReadarrData { + quality_profile_map: BiMap::from_iter([ + (2i64, "Lossless".to_owned()), + (1i64, "Standard".to_owned()), + ]), + metadata_profile_map: BiMap::from_iter([ + (2i64, "None".to_owned()), + (1i64, "Standard".to_owned()), + ]), + ..ReadarrData::default() + }; + let root_folder_1 = RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }; + readarr_data.root_folders.set_items(vec![ + root_folder_1.clone(), + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + + let add_author_modal = AddAuthorModal::from(&readarr_data); + + assert_eq!( + *add_author_modal.monitor_list.current_selection(), + MonitorType::default() + ); + assert_eq!( + *add_author_modal.monitor_new_items_list.current_selection(), + NewItemMonitorType::default() + ); + assert_str_eq!( + add_author_modal.quality_profile_list.current_selection(), + "Standard" + ); + assert_str_eq!( + add_author_modal.metadata_profile_list.current_selection(), + "Standard" + ); + assert_eq!( + add_author_modal.root_folder_list.current_selection(), + &root_folder_1 + ); + assert_is_empty!(add_author_modal.tags.text); + } + + #[test] + fn test_edit_author_modal_from_readarr_data() { + let mut readarr_data = ReadarrData { + quality_profile_map: BiMap::from_iter([ + (1i64, "HD - 1080p".to_owned()), + (2i64, "Any".to_owned()), + ]), + metadata_profile_map: BiMap::from_iter([ + (1i64, "Standard".to_owned()), + (2i64, "None".to_owned()), + ]), + tags_map: BiMap::from_iter([(1i64, "usenet".to_owned())]), + ..ReadarrData::default() + }; + let author = Author { + id: 1, + monitored: true, + monitor_new_items: NewItemMonitorType::New, + quality_profile_id: 1, + metadata_profile_id: 1, + path: "/nfs/books/test_author".to_owned(), + tags: vec![Number::from(1)], + ..Author::default() + }; + readarr_data.authors.set_items(vec![author]); + + let edit_author_modal = EditAuthorModal::from(&readarr_data); + + assert_some_eq_x!(&edit_author_modal.monitored, &true); + assert_eq!( + *edit_author_modal.monitor_list.current_selection(), + NewItemMonitorType::New + ); + assert_str_eq!( + edit_author_modal.quality_profile_list.current_selection(), + "HD - 1080p" + ); + assert_str_eq!( + edit_author_modal.metadata_profile_list.current_selection(), + "Standard" + ); + assert_str_eq!(edit_author_modal.path.text, "/nfs/books/test_author"); + assert_str_eq!(edit_author_modal.tags.text, "usenet"); + } + + #[rstest] + fn test_edit_indexer_modal_from_readarr_data(#[values(true, false)] seed_ratio_present: bool) { + let mut readarr_data = ReadarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..ReadarrData::default() + }; + let mut fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + ]; + + if seed_ratio_present { + fields.push(IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(Value::from(1.2f64)), + }); + } + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + priority: 1, + ..Indexer::default() + }; + readarr_data.indexers.set_items(vec![indexer]); + + let edit_indexer_modal = EditIndexerModal::from(&readarr_data); + + assert_str_eq!(edit_indexer_modal.name.text, "Test"); + assert_some_eq_x!(&edit_indexer_modal.enable_rss, &true); + assert_some_eq_x!(&edit_indexer_modal.enable_automatic_search, &true); + assert_some_eq_x!(&edit_indexer_modal.enable_interactive_search, &true); + assert_eq!(edit_indexer_modal.priority, 1); + assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); + assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); + + if seed_ratio_present { + assert_str_eq!(edit_indexer_modal.seed_ratio.text, "1.2"); + } else { + assert_is_empty!(edit_indexer_modal.seed_ratio.text); + } + } + + #[test] + fn test_edit_indexer_modal_from_readarr_data_seed_ratio_value_is_none() { + let mut readarr_data = ReadarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..ReadarrData::default() + }; + let fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: None, + }, + ]; + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + priority: 1, + ..Indexer::default() + }; + readarr_data.indexers.set_items(vec![indexer]); + + let edit_indexer_modal = EditIndexerModal::from(&readarr_data); + + assert_str_eq!(edit_indexer_modal.name.text, "Test"); + assert_some_eq_x!(&edit_indexer_modal.enable_rss, &true); + assert_some_eq_x!(&edit_indexer_modal.enable_automatic_search, &true); + assert_some_eq_x!(&edit_indexer_modal.enable_interactive_search, &true); + assert_eq!(edit_indexer_modal.priority, 1); + assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); + assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); + assert_is_empty!(edit_indexer_modal.seed_ratio.text); + } + + #[test] + fn test_add_readarr_root_folder_modal_from_readarr_data() { + let readarr_data = ReadarrData { + quality_profile_map: BiMap::from_iter([ + (2i64, "Lossless".to_owned()), + (1i64, "Standard".to_owned()), + ]), + metadata_profile_map: BiMap::from_iter([ + (2i64, "None".to_owned()), + (1i64, "Standard".to_owned()), + ]), + ..ReadarrData::default() + }; + + let add_root_folder_modal = AddReadarrRootFolderModal::from(&readarr_data); + + assert_eq!( + *add_root_folder_modal.monitor_list.current_selection(), + MonitorType::default() + ); + assert_eq!( + *add_root_folder_modal + .monitor_new_items_list + .current_selection(), + NewItemMonitorType::default() + ); + assert_str_eq!( + add_root_folder_modal + .quality_profile_list + .current_selection(), + "Standard" + ); + assert_str_eq!( + add_root_folder_modal + .metadata_profile_list + .current_selection(), + "Standard" + ); + assert_is_empty!(add_root_folder_modal.name.text); + assert_is_empty!(add_root_folder_modal.path.text); + assert_is_empty!(add_root_folder_modal.tags.text); + } + + #[test] + fn test_book_details_modal_default() { + let book_details_modal = BookDetailsModal::default(); + + assert_is_empty!(book_details_modal.editions); + assert_none!(book_details_modal.edition_details_modal); + assert_is_empty!(book_details_modal.book_files); + assert_is_empty!(book_details_modal.book_releases); + assert_is_empty!(book_details_modal.book_history); + + assert_eq!(book_details_modal.book_details_tabs.tabs.len(), 3); + + assert_str_eq!( + book_details_modal.book_details_tabs.tabs[0].title, + "Editions" + ); + assert_eq!( + book_details_modal.book_details_tabs.tabs[0].route, + ActiveReadarrBlock::BookDetails.into() + ); + assert_some_eq_x!( + &book_details_modal.book_details_tabs.tabs[0].contextual_help, + &BOOK_DETAILS_CONTEXT_CLUES + ); + assert_none!(book_details_modal.book_details_tabs.tabs[0].config); + + assert_str_eq!( + book_details_modal.book_details_tabs.tabs[1].title, + "History" + ); + assert_eq!( + book_details_modal.book_details_tabs.tabs[1].route, + ActiveReadarrBlock::BookHistory.into() + ); + assert_some_eq_x!( + &book_details_modal.book_details_tabs.tabs[1].contextual_help, + &BOOK_HISTORY_CONTEXT_CLUES + ); + assert_none!(book_details_modal.book_details_tabs.tabs[1].config); + + assert_str_eq!( + book_details_modal.book_details_tabs.tabs[2].title, + "Manual Search" + ); + assert_eq!( + book_details_modal.book_details_tabs.tabs[2].route, + ActiveReadarrBlock::ManualBookSearch.into() + ); + assert_some_eq_x!( + &book_details_modal.book_details_tabs.tabs[2].contextual_help, + &MANUAL_BOOK_SEARCH_CONTEXT_CLUES + ); + assert_none!(book_details_modal.book_details_tabs.tabs[2].config); + } + + #[test] + fn test_edition_details_modal_default() { + let edition_details_modal = EditionDetailsModal::default(); + + assert_is_empty!(edition_details_modal.edition_details); + } +} diff --git a/src/models/servarr_data/readarr/readarr_data.rs b/src/models/servarr_data/readarr/readarr_data.rs new file mode 100644 index 00000000..08a98bd3 --- /dev/null +++ b/src/models/servarr_data/readarr/readarr_data.rs @@ -0,0 +1,980 @@ +use serde_json::Number; + +use super::modals::{AddAuthorModal, AddReadarrRootFolderModal, BookDetailsModal, EditAuthorModal}; +use crate::app::context_clues::{ + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, +}; +use crate::app::readarr::readarr_context_clues::{ + AUTHOR_DETAILS_CONTEXT_CLUES, AUTHOR_HISTORY_CONTEXT_CLUES, AUTHORS_CONTEXT_CLUES, + MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES, +}; +use crate::models::readarr_models::{BlocklistItem, ReadarrRelease, ReadarrTask}; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_models::{IndexerSettings, QueueEvent}; +use crate::models::stateful_list::StatefulList; +use crate::models::{ + BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState, + readarr_models::{AddAuthorSearchResult, Author, Book, DownloadRecord, ReadarrHistoryItem}, + servarr_data::modals::IndexerTestResultModalItem, + servarr_models::{DiskSpace, Indexer, RootFolder}, + stateful_table::StatefulTable, +}; +use crate::network::readarr_network::ReadarrEvent; +use bimap::BiMap; +use chrono::{DateTime, Utc}; +use itertools::Itertools; +use strum::EnumIter; +#[cfg(test)] +use { + super::modals::EditionDetailsModal, + crate::models::readarr_models::{ + AuthorStatus, BookFile, DownloadStatus, Edition, MonitorType, NewItemMonitorType, + ReadarrHistoryEventType, ReadarrTaskName, + }, + crate::models::servarr_models::{IndexerField, Quality, QualityWrapper}, + crate::models::stateful_table::SortOption, + crate::network::servarr_test_utils::{ + diskspace, indexer_settings, indexer_test_result, queued_event, + }, + crate::network::sonarr_network::sonarr_network_test_utils::test_utils::updates, + crate::sort_option, + serde_json::json, + strum::{Display, EnumString, IntoEnumIterator}, +}; + +#[cfg(test)] +#[path = "readarr_data_tests.rs"] +mod readarr_data_tests; + +pub struct ReadarrData<'a> { + pub add_author_modal: Option, + pub add_author_search: Option, + pub add_import_list_exclusion: bool, + pub add_root_folder_modal: Option, + pub add_searched_authors: Option>, + pub author_history: StatefulTable, + pub author_info_tabs: TabState, + pub author_releases: StatefulTable, + pub authors: StatefulTable, + pub blocklist: StatefulTable, + pub book_details_modal: Option, + pub books: StatefulTable, + pub delete_files: bool, + pub disk_space_vec: Vec, + pub downloads: StatefulTable, + pub edit_author_modal: Option, + pub edit_indexer_modal: Option, + pub history: StatefulTable, + pub indexer_settings: Option, + pub indexer_test_all_results: Option>, + pub indexer_test_errors: Option, + pub indexers: StatefulTable, + pub log_details: StatefulList, + pub logs: StatefulList, + pub main_tabs: TabState, + pub metadata_profile_map: BiMap, + pub prompt_confirm: bool, + pub prompt_confirm_action: Option, + pub quality_profile_map: BiMap, + pub queued_events: StatefulTable, + pub root_folders: StatefulTable, + pub selected_block: BlockSelectionState<'a, ActiveReadarrBlock>, + pub start_time: DateTime, + pub tags_map: BiMap, + pub tasks: StatefulTable, + pub updates: ScrollableText, + pub version: String, +} + +impl ReadarrData<'_> { + pub fn reset_delete_preferences(&mut self) { + self.delete_files = false; + self.add_import_list_exclusion = false; + } + + pub fn reset_author_info_tabs(&mut self) { + self.books = StatefulTable::default(); + self.author_releases = StatefulTable::default(); + self.author_history = StatefulTable::default(); + self.author_info_tabs.index = 0; + } + + pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String { + tag_ids + .iter() + .filter_map(|id| { + let id = id.as_i64()?; + self.tags_map.get_by_left(&id).cloned() + }) + .collect::>() + .join(", ") + } + + pub fn sorted_quality_profile_names(&self) -> Vec { + self + .quality_profile_map + .iter() + .sorted_by_key(|(id, _)| *id) + .map(|(_, name)| name) + .cloned() + .collect() + } + + pub fn sorted_metadata_profile_names(&self) -> Vec { + self + .metadata_profile_map + .iter() + .sorted_by_key(|(id, _)| *id) + .map(|(_, name)| name) + .cloned() + .collect() + } +} + +impl<'a> Default for ReadarrData<'a> { + fn default() -> ReadarrData<'a> { + ReadarrData { + add_author_modal: None, + add_author_search: None, + add_import_list_exclusion: false, + add_root_folder_modal: None, + add_searched_authors: None, + author_history: StatefulTable::default(), + author_releases: StatefulTable::default(), + authors: StatefulTable::default(), + blocklist: StatefulTable::default(), + book_details_modal: None, + books: StatefulTable::default(), + delete_files: false, + disk_space_vec: Vec::new(), + downloads: StatefulTable::default(), + edit_author_modal: None, + edit_indexer_modal: None, + history: StatefulTable::default(), + indexer_settings: None, + indexer_test_all_results: None, + indexer_test_errors: None, + indexers: StatefulTable::default(), + log_details: StatefulList::default(), + logs: StatefulList::default(), + metadata_profile_map: BiMap::new(), + prompt_confirm: false, + prompt_confirm_action: None, + quality_profile_map: BiMap::new(), + queued_events: StatefulTable::default(), + root_folders: StatefulTable::default(), + selected_block: BlockSelectionState::default(), + start_time: DateTime::default(), + tags_map: BiMap::new(), + tasks: StatefulTable::default(), + updates: ScrollableText::default(), + version: String::new(), + main_tabs: TabState::new(vec![ + TabRoute { + title: "Library".to_string(), + route: ActiveReadarrBlock::Authors.into(), + contextual_help: Some(&AUTHORS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Downloads".to_string(), + route: ActiveReadarrBlock::Downloads.into(), + contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Blocklist".to_string(), + route: ActiveReadarrBlock::Blocklist.into(), + contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "History".to_string(), + route: ActiveReadarrBlock::History.into(), + contextual_help: Some(&HISTORY_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Root Folders".to_string(), + route: ActiveReadarrBlock::RootFolders.into(), + contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Indexers".to_string(), + route: ActiveReadarrBlock::Indexers.into(), + contextual_help: Some(&INDEXERS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "System".to_string(), + route: ActiveReadarrBlock::System.into(), + contextual_help: Some(&SYSTEM_CONTEXT_CLUES), + config: None, + }, + ]), + author_info_tabs: TabState::new(vec![ + TabRoute { + title: "Books".to_string(), + route: ActiveReadarrBlock::AuthorDetails.into(), + contextual_help: Some(&AUTHOR_DETAILS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "History".to_string(), + route: ActiveReadarrBlock::AuthorHistory.into(), + contextual_help: Some(&AUTHOR_HISTORY_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Author Search".to_string(), + route: ActiveReadarrBlock::ManualAuthorSearch.into(), + contextual_help: Some(&MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES), + config: None, + }, + ]), + } + } +} + +#[cfg(test)] +impl ReadarrData<'_> { + pub fn test_default_fully_populated() -> Self { + let quality_profile_map = BiMap::from_iter([(1i64, "Standard".to_owned())]); + let metadata_profile_map = BiMap::from_iter([(1i64, "Standard".to_owned())]); + let tags_map = BiMap::from_iter([(1i64, "alex".to_owned())]); + let root_folder = RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }; + let quality = QualityWrapper { + quality: Quality { + name: "EPUB".to_owned(), + }, + }; + let date = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); + let author = Author { + id: 1, + author_name: "Test Author".into(), + foreign_author_id: "test-foreign-id".to_owned(), + status: AuthorStatus::Continuing, + overview: Some("some interesting description of the author".to_owned()), + path: "/nfs/books/Test Author".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + monitored: true, + monitor_new_items: NewItemMonitorType::All, + genres: vec!["science fiction".to_owned()], + tags: vec![Number::from(1)], + ..Author::default() + }; + let book = Book { + id: 1, + title: "Test Book".into(), + author_id: 1, + foreign_book_id: "test-foreign-book-id".to_owned(), + monitored: true, + any_edition_ok: true, + page_count: Some(288), + release_date: Some(date), + ..Book::default() + }; + let readarr_history_item = ReadarrHistoryItem { + id: 1, + author_id: 1, + book_id: 1, + source_title: "Test Source Title".into(), + quality: quality.clone(), + date, + event_type: ReadarrHistoryEventType::Grabbed, + ..ReadarrHistoryItem::default() + }; + let torrent_release = ReadarrRelease { + guid: "1234".to_owned(), + protocol: "torrent".to_owned(), + age: 1, + title: "Test Torrent Release".into(), + author_name: Some("Test Author".to_owned()), + book_title: Some("Test Book".to_owned()), + indexer: "kickass torrents".to_owned(), + indexer_id: 1, + size: 1234, + rejected: true, + rejections: Some(vec!["Unknown quality profile".to_owned()]), + seeders: Some(Number::from(2)), + leechers: Some(Number::from(1)), + quality: quality.clone(), + }; + let usenet_release = ReadarrRelease { + guid: "5678".to_owned(), + protocol: "usenet".to_owned(), + title: "Test Usenet Release".into(), + indexer: "DrunkenSlug".to_owned(), + indexer_id: 2, + seeders: None, + leechers: None, + ..torrent_release.clone() + }; + let blocklist_item = BlocklistItem { + id: 1, + author_id: 1, + book_ids: Some(vec![Number::from(1)]), + source_title: "Test Source Title".to_owned(), + quality: quality.clone(), + date, + protocol: "usenet".to_owned(), + indexer: "DrunkenSlug".to_owned(), + message: "test message".to_owned(), + author: author.clone(), + }; + let download_record = DownloadRecord { + title: "Test Download Title".to_owned(), + status: DownloadStatus::Downloading, + id: 1, + book_id: Some(Number::from(1)), + author_id: Some(Number::from(1)), + size: 3543348019f64, + sizeleft: 1771674009f64, + output_path: Some("/nfs/books/Test Author".into()), + indexer: "kickass torrents".to_owned(), + download_client: Some("transmission".to_owned()), + }; + let indexer = Indexer { + id: 1, + name: Some("Test Indexer".to_owned()), + implementation: Some("Torznab".to_owned()), + implementation_name: Some("Torznab".to_owned()), + config_contract: Some("TorznabSettings".to_owned()), + supports_rss: true, + supports_search: true, + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + protocol: "torrent".to_owned(), + priority: 25, + download_client_id: 0, + tags: vec![Number::from(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")), + }, + ]), + }; + let add_author_search_result = AddAuthorSearchResult { + foreign_author_id: "test-foreign-id".to_owned(), + author_name: "Test Author".into(), + status: AuthorStatus::Continuing, + overview: Some("some interesting description of the author".to_owned()), + genres: vec!["science fiction".to_owned()], + ..AddAuthorSearchResult::default() + }; + let task = ReadarrTask { + name: "Backup".to_owned(), + task_name: ReadarrTaskName::Backup, + interval: 60, + last_execution: date, + last_duration: "00:00:00.5111547".to_owned(), + next_execution: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T22:29:16Z").unwrap()), + }; + let log_line = "2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process"; + + let mut add_author_modal = AddAuthorModal { + tags: "usenet, testing".into(), + ..AddAuthorModal::default() + }; + add_author_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_author_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_author_modal + .metadata_profile_list + .set_items(vec!["Standard".to_owned()]); + add_author_modal + .quality_profile_list + .set_items(vec!["Standard".to_owned()]); + add_author_modal + .root_folder_list + .set_items(vec![root_folder.clone()]); + + let mut edit_author_modal = EditAuthorModal { + monitored: Some(true), + path: "/nfs/books".into(), + tags: "alex".into(), + ..EditAuthorModal::default() + }; + edit_author_modal + .monitor_list + .set_items(NewItemMonitorType::iter().collect()); + edit_author_modal + .quality_profile_list + .set_items(vec!["Standard".to_owned()]); + edit_author_modal + .metadata_profile_list + .set_items(vec!["Standard".to_owned()]); + + let mut add_root_folder_modal = AddReadarrRootFolderModal { + name: "Test Root Folder".into(), + path: "/nfs/books".into(), + tags: "test".into(), + ..AddReadarrRootFolderModal::default() + }; + add_root_folder_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_root_folder_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_root_folder_modal + .quality_profile_list + .set_items(vec!["Standard".to_owned()]); + add_root_folder_modal + .metadata_profile_list + .set_items(vec!["Standard".to_owned()]); + + let edition_details_modal = EditionDetailsModal { + edition_details: ScrollableText::with_string("Some details".to_owned()), + }; + + let mut book_details_modal = BookDetailsModal { + edition_details_modal: Some(edition_details_modal), + ..BookDetailsModal::default() + }; + book_details_modal.editions.set_items(vec![Edition { + id: 1, + book_id: 1, + foreign_edition_id: "test-foreign-edition-id".to_owned(), + monitored: true, + is_ebook: true, + title: "Test Edition".to_owned(), + page_count: Some(288), + release_date: Some(date), + ..Edition::default() + }]); + book_details_modal.editions.search = Some("edition search".into()); + book_details_modal.book_files.set_items(vec![BookFile { + id: 1, + author_id: 1, + book_id: 1, + path: "/nfs/books/Test Author/Test Book.epub".to_owned(), + size: 3543348019, + date_added: date, + quality: quality.clone(), + media_info: None, + quality_cutoff_not_met: false, + }]); + book_details_modal + .book_history + .set_items(vec![readarr_history_item.clone()]); + book_details_modal.book_history.search = Some("book history search".into()); + book_details_modal.book_history.filter = Some("book history filter".into()); + book_details_modal + .book_history + .sorting(vec![sort_option!(id)]); + book_details_modal + .book_releases + .set_items(vec![torrent_release.clone(), usenet_release.clone()]); + book_details_modal + .book_releases + .sorting(vec![sort_option!(indexer_id)]); + + let edit_indexer_modal = EditIndexerModal { + name: "DrunkenSlug".into(), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: "http://127.0.0.1:9696/1/".into(), + api_key: "someApiKey".into(), + seed_ratio: "ratio".into(), + tags: "25".into(), + priority: 1, + }; + + let mut indexer_test_all_results = StatefulTable::default(); + indexer_test_all_results.set_items(vec![indexer_test_result()]); + + let mut readarr_data = ReadarrData { + add_author_modal: Some(add_author_modal), + add_root_folder_modal: Some(add_root_folder_modal), + book_details_modal: Some(book_details_modal), + delete_files: true, + disk_space_vec: vec![diskspace()], + edit_author_modal: Some(edit_author_modal), + edit_indexer_modal: Some(edit_indexer_modal), + indexer_settings: Some(indexer_settings()), + indexer_test_all_results: Some(indexer_test_all_results), + indexer_test_errors: Some("error".to_string()), + metadata_profile_map, + quality_profile_map, + start_time: date, + tags_map, + updates: updates(), + version: "1.2.3.4".to_owned(), + ..ReadarrData::default() + }; + readarr_data + .author_history + .set_items(vec![readarr_history_item.clone()]); + readarr_data.author_history.sorting(vec![sort_option!(id)]); + readarr_data.author_history.search = Some("author history search".into()); + readarr_data.author_history.filter = Some("author history filter".into()); + readarr_data + .author_releases + .set_items(vec![torrent_release, usenet_release]); + readarr_data + .author_releases + .sorting(vec![sort_option!(indexer_id)]); + readarr_data.authors.set_items(vec![author]); + readarr_data.authors.sorting(vec![SortOption { + name: "Name", + cmp_fn: Some(|a: &Author, b: &Author| a.author_name.text.cmp(&b.author_name.text)), + }]); + readarr_data.authors.search = Some("author search".into()); + readarr_data.authors.filter = Some("author filter".into()); + readarr_data.blocklist.set_items(vec![blocklist_item]); + readarr_data.blocklist.sorting(vec![sort_option!(id)]); + readarr_data.books.set_items(vec![book]); + readarr_data.books.search = Some("book search".into()); + readarr_data.downloads.set_items(vec![download_record]); + readarr_data.history.set_items(vec![readarr_history_item]); + readarr_data.history.sorting(vec![SortOption { + name: "Date", + cmp_fn: Some(|a: &ReadarrHistoryItem, b: &ReadarrHistoryItem| a.date.cmp(&b.date)), + }]); + readarr_data.history.search = Some("test search".into()); + readarr_data.history.filter = Some("test filter".into()); + readarr_data.indexers.set_items(vec![indexer]); + readarr_data.root_folders.set_items(vec![root_folder]); + readarr_data.queued_events.set_items(vec![queued_event()]); + readarr_data.add_author_search = Some("Test Author".into()); + let mut add_searched_authors = StatefulTable::default(); + add_searched_authors.set_items(vec![add_author_search_result]); + readarr_data.add_searched_authors = Some(add_searched_authors); + readarr_data.logs.set_items(vec![log_line.into()]); + readarr_data.log_details.set_items(vec![log_line.into()]); + readarr_data.tasks.set_items(vec![task]); + + readarr_data + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)] +#[cfg_attr(test, derive(Display, EnumString))] +pub enum ActiveReadarrBlock { + #[default] + Authors, + AddAuthorAlreadyInLibrary, + AddAuthorConfirmPrompt, + AddAuthorEmptySearchResults, + AddAuthorPrompt, + AddAuthorSearchInput, + AddAuthorSearchResults, + AddAuthorSelectMetadataProfile, + AddAuthorSelectMonitor, + AddAuthorSelectMonitorNewItems, + AddAuthorSelectQualityProfile, + AddAuthorSelectRootFolder, + AddAuthorTagsInput, + AddRootFolderConfirmPrompt, + AddRootFolderNameInput, + AddRootFolderPathInput, + AddRootFolderPrompt, + AddRootFolderSelectMetadataProfile, + AddRootFolderSelectMonitor, + AddRootFolderSelectMonitorNewItems, + AddRootFolderSelectQualityProfile, + AddRootFolderTagsInput, + AllIndexerSettingsPrompt, + AuthorDetails, + AuthorHistory, + AuthorHistoryDetails, + AuthorHistorySortPrompt, + AuthorsSortPrompt, + AutomaticallySearchAuthorPrompt, + AutomaticallySearchBookPrompt, + Blocklist, + BlocklistClearAllItemsPrompt, + BlocklistItemDetails, + BlocklistSortPrompt, + BookDetails, + BookEditionDetails, + BookFileInfo, + BookHistory, + BookHistoryDetails, + BookHistorySortPrompt, + DeleteAuthorConfirmPrompt, + DeleteAuthorPrompt, + DeleteAuthorToggleAddListExclusion, + DeleteAuthorToggleDeleteFile, + DeleteBlocklistItemPrompt, + DeleteBookConfirmPrompt, + DeleteBookFilePrompt, + DeleteBookPrompt, + DeleteBookToggleAddListExclusion, + DeleteBookToggleDeleteFile, + DeleteDownloadPrompt, + DeleteIndexerPrompt, + DeleteRootFolderPrompt, + Downloads, + EditAuthorConfirmPrompt, + EditAuthorPathInput, + EditAuthorPrompt, + EditAuthorSelectMetadataProfile, + EditAuthorSelectMonitorNewItems, + EditAuthorSelectQualityProfile, + EditAuthorTagsInput, + EditAuthorToggleMonitored, + EditIndexerApiKeyInput, + EditIndexerConfirmPrompt, + EditIndexerNameInput, + EditIndexerPriorityInput, + EditIndexerPrompt, + EditIndexerSeedRatioInput, + EditIndexerTagsInput, + EditIndexerToggleEnableAutomaticSearch, + EditIndexerToggleEnableInteractiveSearch, + EditIndexerToggleEnableRss, + EditIndexerUrlInput, + FilterAuthorHistory, + FilterAuthorHistoryError, + FilterAuthors, + FilterAuthorsError, + FilterBookHistory, + FilterBookHistoryError, + FilterHistory, + FilterHistoryError, + History, + HistoryItemDetails, + HistorySortPrompt, + Indexers, + IndexerSettingsConfirmPrompt, + IndexerSettingsMaximumSizeInput, + IndexerSettingsMinimumAgeInput, + IndexerSettingsRetentionInput, + IndexerSettingsRssSyncIntervalInput, + ManualAuthorSearch, + ManualAuthorSearchConfirmPrompt, + ManualAuthorSearchSortPrompt, + ManualBookSearch, + ManualBookSearchConfirmPrompt, + ManualBookSearchSortPrompt, + RootFolders, + SearchAuthorHistory, + SearchAuthorHistoryError, + SearchAuthors, + SearchAuthorsError, + SearchBookHistory, + SearchBookHistoryError, + SearchBooks, + SearchBooksError, + SearchEditions, + SearchEditionsError, + SearchHistory, + SearchHistoryError, + System, + SystemLogs, + SystemQueuedEvents, + SystemTasks, + SystemTaskStartConfirmPrompt, + SystemUpdates, + TestAllIndexers, + TestIndexer, + UpdateAllAuthorsPrompt, + UpdateAndScanAuthorPrompt, + UpdateDownloadsPrompt, +} + +pub static LIBRARY_BLOCKS: [ActiveReadarrBlock; 7] = [ + ActiveReadarrBlock::Authors, + ActiveReadarrBlock::AuthorsSortPrompt, + ActiveReadarrBlock::FilterAuthors, + ActiveReadarrBlock::FilterAuthorsError, + ActiveReadarrBlock::SearchAuthors, + ActiveReadarrBlock::SearchAuthorsError, + ActiveReadarrBlock::UpdateAllAuthorsPrompt, +]; + +pub static AUTHOR_DETAILS_BLOCKS: [ActiveReadarrBlock; 15] = [ + ActiveReadarrBlock::AuthorDetails, + ActiveReadarrBlock::AuthorHistory, + ActiveReadarrBlock::AuthorHistoryDetails, + ActiveReadarrBlock::AuthorHistorySortPrompt, + ActiveReadarrBlock::AutomaticallySearchAuthorPrompt, + ActiveReadarrBlock::FilterAuthorHistory, + ActiveReadarrBlock::FilterAuthorHistoryError, + ActiveReadarrBlock::ManualAuthorSearch, + ActiveReadarrBlock::ManualAuthorSearchConfirmPrompt, + ActiveReadarrBlock::ManualAuthorSearchSortPrompt, + ActiveReadarrBlock::SearchBooks, + ActiveReadarrBlock::SearchBooksError, + ActiveReadarrBlock::SearchAuthorHistory, + ActiveReadarrBlock::SearchAuthorHistoryError, + ActiveReadarrBlock::UpdateAndScanAuthorPrompt, +]; + +pub static BOOK_DETAILS_BLOCKS: [ActiveReadarrBlock; 16] = [ + ActiveReadarrBlock::BookDetails, + ActiveReadarrBlock::BookHistory, + ActiveReadarrBlock::BookHistoryDetails, + ActiveReadarrBlock::BookHistorySortPrompt, + ActiveReadarrBlock::AutomaticallySearchBookPrompt, + ActiveReadarrBlock::DeleteBookFilePrompt, + ActiveReadarrBlock::FilterBookHistory, + ActiveReadarrBlock::FilterBookHistoryError, + ActiveReadarrBlock::BookFileInfo, + ActiveReadarrBlock::ManualBookSearch, + ActiveReadarrBlock::ManualBookSearchConfirmPrompt, + ActiveReadarrBlock::ManualBookSearchSortPrompt, + ActiveReadarrBlock::SearchBookHistory, + ActiveReadarrBlock::SearchBookHistoryError, + ActiveReadarrBlock::SearchEditions, + ActiveReadarrBlock::SearchEditionsError, +]; + +pub static EDITION_DETAILS_BLOCKS: [ActiveReadarrBlock; 1] = + [ActiveReadarrBlock::BookEditionDetails]; + +pub static ADD_AUTHOR_BLOCKS: [ActiveReadarrBlock; 12] = [ + ActiveReadarrBlock::AddAuthorAlreadyInLibrary, + ActiveReadarrBlock::AddAuthorConfirmPrompt, + ActiveReadarrBlock::AddAuthorEmptySearchResults, + ActiveReadarrBlock::AddAuthorPrompt, + ActiveReadarrBlock::AddAuthorSearchInput, + ActiveReadarrBlock::AddAuthorSearchResults, + ActiveReadarrBlock::AddAuthorSelectMetadataProfile, + ActiveReadarrBlock::AddAuthorSelectMonitor, + ActiveReadarrBlock::AddAuthorSelectMonitorNewItems, + ActiveReadarrBlock::AddAuthorSelectQualityProfile, + ActiveReadarrBlock::AddAuthorSelectRootFolder, + ActiveReadarrBlock::AddAuthorTagsInput, +]; + +pub const ADD_AUTHOR_SELECTION_BLOCKS: &[&[ActiveReadarrBlock]] = &[ + &[ActiveReadarrBlock::AddAuthorSelectRootFolder], + &[ActiveReadarrBlock::AddAuthorSelectMonitor], + &[ActiveReadarrBlock::AddAuthorSelectMonitorNewItems], + &[ActiveReadarrBlock::AddAuthorSelectQualityProfile], + &[ActiveReadarrBlock::AddAuthorSelectMetadataProfile], + &[ActiveReadarrBlock::AddAuthorTagsInput], + &[ActiveReadarrBlock::AddAuthorConfirmPrompt], +]; + +pub static DELETE_AUTHOR_BLOCKS: [ActiveReadarrBlock; 4] = [ + ActiveReadarrBlock::DeleteAuthorPrompt, + ActiveReadarrBlock::DeleteAuthorConfirmPrompt, + ActiveReadarrBlock::DeleteAuthorToggleDeleteFile, + ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion, +]; + +pub const DELETE_AUTHOR_SELECTION_BLOCKS: &[&[ActiveReadarrBlock]] = &[ + &[ActiveReadarrBlock::DeleteAuthorToggleDeleteFile], + &[ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion], + &[ActiveReadarrBlock::DeleteAuthorConfirmPrompt], +]; + +pub static DELETE_BOOK_BLOCKS: [ActiveReadarrBlock; 4] = [ + ActiveReadarrBlock::DeleteBookPrompt, + ActiveReadarrBlock::DeleteBookConfirmPrompt, + ActiveReadarrBlock::DeleteBookToggleDeleteFile, + ActiveReadarrBlock::DeleteBookToggleAddListExclusion, +]; + +pub const DELETE_BOOK_SELECTION_BLOCKS: &[&[ActiveReadarrBlock]] = &[ + &[ActiveReadarrBlock::DeleteBookToggleDeleteFile], + &[ActiveReadarrBlock::DeleteBookToggleAddListExclusion], + &[ActiveReadarrBlock::DeleteBookConfirmPrompt], +]; + +pub static EDIT_AUTHOR_BLOCKS: [ActiveReadarrBlock; 8] = [ + ActiveReadarrBlock::EditAuthorPrompt, + ActiveReadarrBlock::EditAuthorConfirmPrompt, + ActiveReadarrBlock::EditAuthorPathInput, + ActiveReadarrBlock::EditAuthorSelectMetadataProfile, + ActiveReadarrBlock::EditAuthorSelectMonitorNewItems, + ActiveReadarrBlock::EditAuthorSelectQualityProfile, + ActiveReadarrBlock::EditAuthorTagsInput, + ActiveReadarrBlock::EditAuthorToggleMonitored, +]; + +pub const EDIT_AUTHOR_SELECTION_BLOCKS: &[&[ActiveReadarrBlock]] = &[ + &[ActiveReadarrBlock::EditAuthorToggleMonitored], + &[ActiveReadarrBlock::EditAuthorSelectMonitorNewItems], + &[ActiveReadarrBlock::EditAuthorSelectQualityProfile], + &[ActiveReadarrBlock::EditAuthorSelectMetadataProfile], + &[ActiveReadarrBlock::EditAuthorPathInput], + &[ActiveReadarrBlock::EditAuthorTagsInput], + &[ActiveReadarrBlock::EditAuthorConfirmPrompt], +]; + +pub static ADD_ROOT_FOLDER_BLOCKS: [ActiveReadarrBlock; 9] = [ + ActiveReadarrBlock::AddRootFolderPrompt, + ActiveReadarrBlock::AddRootFolderConfirmPrompt, + ActiveReadarrBlock::AddRootFolderNameInput, + ActiveReadarrBlock::AddRootFolderPathInput, + ActiveReadarrBlock::AddRootFolderSelectMonitor, + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems, + ActiveReadarrBlock::AddRootFolderSelectQualityProfile, + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile, + ActiveReadarrBlock::AddRootFolderTagsInput, +]; + +pub const ADD_ROOT_FOLDER_SELECTION_BLOCKS: &[&[ActiveReadarrBlock]] = &[ + &[ActiveReadarrBlock::AddRootFolderNameInput], + &[ActiveReadarrBlock::AddRootFolderPathInput], + &[ActiveReadarrBlock::AddRootFolderSelectMonitor], + &[ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems], + &[ActiveReadarrBlock::AddRootFolderSelectQualityProfile], + &[ActiveReadarrBlock::AddRootFolderSelectMetadataProfile], + &[ActiveReadarrBlock::AddRootFolderTagsInput], + &[ActiveReadarrBlock::AddRootFolderConfirmPrompt], +]; + +pub static ROOT_FOLDERS_BLOCKS: [ActiveReadarrBlock; 2] = [ + ActiveReadarrBlock::RootFolders, + ActiveReadarrBlock::DeleteRootFolderPrompt, +]; + +pub static BLOCKLIST_BLOCKS: [ActiveReadarrBlock; 5] = [ + ActiveReadarrBlock::Blocklist, + ActiveReadarrBlock::BlocklistItemDetails, + ActiveReadarrBlock::DeleteBlocklistItemPrompt, + ActiveReadarrBlock::BlocklistClearAllItemsPrompt, + ActiveReadarrBlock::BlocklistSortPrompt, +]; + +pub static DOWNLOADS_BLOCKS: [ActiveReadarrBlock; 3] = [ + ActiveReadarrBlock::Downloads, + ActiveReadarrBlock::DeleteDownloadPrompt, + ActiveReadarrBlock::UpdateDownloadsPrompt, +]; + +pub static HISTORY_BLOCKS: [ActiveReadarrBlock; 7] = [ + ActiveReadarrBlock::History, + ActiveReadarrBlock::HistoryItemDetails, + ActiveReadarrBlock::HistorySortPrompt, + ActiveReadarrBlock::SearchHistory, + ActiveReadarrBlock::SearchHistoryError, + ActiveReadarrBlock::FilterHistory, + ActiveReadarrBlock::FilterHistoryError, +]; + +pub static EDIT_INDEXER_BLOCKS: [ActiveReadarrBlock; 11] = [ + ActiveReadarrBlock::EditIndexerPrompt, + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ActiveReadarrBlock::EditIndexerApiKeyInput, + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + ActiveReadarrBlock::EditIndexerToggleEnableRss, + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveReadarrBlock::EditIndexerPriorityInput, + ActiveReadarrBlock::EditIndexerUrlInput, + ActiveReadarrBlock::EditIndexerTagsInput, +]; + +pub const EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: &[&[ActiveReadarrBlock]] = &[ + &[ + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveReadarrBlock::EditIndexerToggleEnableRss, + ActiveReadarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + ], + &[ + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveReadarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveReadarrBlock::EditIndexerPriorityInput, + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ], + &[ + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ], +]; + +pub const EDIT_INDEXER_NZB_SELECTION_BLOCKS: &[&[ActiveReadarrBlock]] = &[ + &[ + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveReadarrBlock::EditIndexerToggleEnableRss, + ActiveReadarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveReadarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveReadarrBlock::EditIndexerPriorityInput, + ], + &[ + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ], +]; + +pub static INDEXER_SETTINGS_BLOCKS: [ActiveReadarrBlock; 6] = [ + ActiveReadarrBlock::AllIndexerSettingsPrompt, + ActiveReadarrBlock::IndexerSettingsConfirmPrompt, + ActiveReadarrBlock::IndexerSettingsMaximumSizeInput, + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput, + ActiveReadarrBlock::IndexerSettingsRetentionInput, + ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput, +]; + +pub const INDEXER_SETTINGS_SELECTION_BLOCKS: &[&[ActiveReadarrBlock]] = &[ + &[ActiveReadarrBlock::IndexerSettingsMinimumAgeInput], + &[ActiveReadarrBlock::IndexerSettingsRetentionInput], + &[ActiveReadarrBlock::IndexerSettingsMaximumSizeInput], + &[ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput], + &[ActiveReadarrBlock::IndexerSettingsConfirmPrompt], +]; + +pub static INDEXERS_BLOCKS: [ActiveReadarrBlock; 3] = [ + ActiveReadarrBlock::Indexers, + ActiveReadarrBlock::DeleteIndexerPrompt, + ActiveReadarrBlock::TestIndexer, +]; + +pub static SYSTEM_DETAILS_BLOCKS: [ActiveReadarrBlock; 5] = [ + ActiveReadarrBlock::SystemLogs, + ActiveReadarrBlock::SystemQueuedEvents, + ActiveReadarrBlock::SystemTasks, + ActiveReadarrBlock::SystemTaskStartConfirmPrompt, + ActiveReadarrBlock::SystemUpdates, +]; + +impl From for Route { + fn from(active_readarr_block: ActiveReadarrBlock) -> Route { + Route::Readarr(active_readarr_block, None) + } +} + +impl From<(ActiveReadarrBlock, Option)> for Route { + fn from(value: (ActiveReadarrBlock, Option)) -> Route { + Route::Readarr(value.0, value.1) + } +} diff --git a/src/models/servarr_data/readarr/readarr_data_tests.rs b/src/models/servarr_data/readarr/readarr_data_tests.rs new file mode 100644 index 00000000..548059f8 --- /dev/null +++ b/src/models/servarr_data/readarr/readarr_data_tests.rs @@ -0,0 +1,789 @@ +#[cfg(test)] +mod tests { + use crate::app::context_clues::{ + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + }; + use crate::app::readarr::readarr_context_clues::{ + AUTHOR_DETAILS_CONTEXT_CLUES, AUTHOR_HISTORY_CONTEXT_CLUES, AUTHORS_CONTEXT_CLUES, + MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES, + }; + use crate::models::readarr_models::{Book, ReadarrHistoryItem, ReadarrRelease}; + use crate::models::servarr_data::readarr::readarr_data::{ + ADD_AUTHOR_BLOCKS, ADD_AUTHOR_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, + ADD_ROOT_FOLDER_SELECTION_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BLOCKLIST_BLOCKS, + BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_AUTHOR_SELECTION_BLOCKS, DELETE_BOOK_BLOCKS, + DELETE_BOOK_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_AUTHOR_BLOCKS, + EDIT_AUTHOR_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDITION_DETAILS_BLOCKS, HISTORY_BLOCKS, + INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, LIBRARY_BLOCKS, + ROOT_FOLDERS_BLOCKS, ReadarrData, SYSTEM_DETAILS_BLOCKS, + }; + use crate::models::{BlockSelectionState, Route}; + use bimap::BiMap; + use chrono::{DateTime, Utc}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::Number; + + #[test] + fn test_from_active_readarr_block_to_route() { + assert_eq!( + Route::from(ActiveReadarrBlock::Authors), + Route::Readarr(ActiveReadarrBlock::Authors, None) + ); + } + + #[test] + fn test_from_tuple_to_route_with_context() { + assert_eq!( + Route::from(( + ActiveReadarrBlock::Authors, + Some(ActiveReadarrBlock::Authors) + )), + Route::Readarr( + ActiveReadarrBlock::Authors, + Some(ActiveReadarrBlock::Authors), + ) + ); + } + + #[test] + fn test_reset_delete_preferences() { + let mut readarr_data = ReadarrData { + delete_files: true, + add_import_list_exclusion: true, + ..ReadarrData::default() + }; + + readarr_data.reset_delete_preferences(); + + assert!(!readarr_data.delete_files); + assert!(!readarr_data.add_import_list_exclusion); + } + + #[test] + fn test_reset_author_info_tabs() { + let mut readarr_data = ReadarrData::default(); + readarr_data.books.set_items(vec![Book::default()]); + readarr_data + .author_releases + .set_items(vec![ReadarrRelease::default()]); + readarr_data + .author_history + .set_items(vec![ReadarrHistoryItem::default()]); + readarr_data.author_info_tabs.index = 1; + + readarr_data.reset_author_info_tabs(); + + assert_is_empty!(readarr_data.books); + assert_is_empty!(readarr_data.author_releases); + assert_is_empty!(readarr_data.author_history); + assert_eq!(readarr_data.author_info_tabs.index, 0); + } + + #[test] + fn test_tag_ids_to_display() { + let mut tags_map = BiMap::new(); + tags_map.insert(3, "test 3".to_owned()); + tags_map.insert(2, "test 2".to_owned()); + tags_map.insert(1, "test 1".to_owned()); + let readarr_data = ReadarrData { + tags_map, + ..ReadarrData::default() + }; + + assert_str_eq!( + readarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), + "test 1, test 2" + ); + } + + #[test] + fn test_sorted_quality_profile_names() { + let mut quality_profile_map = BiMap::new(); + quality_profile_map.insert(3, "test 1".to_owned()); + quality_profile_map.insert(2, "test 2".to_owned()); + quality_profile_map.insert(1, "test 3".to_owned()); + let readarr_data = ReadarrData { + quality_profile_map, + ..ReadarrData::default() + }; + let expected_quality_profile_vec = vec![ + "test 3".to_owned(), + "test 2".to_owned(), + "test 1".to_owned(), + ]; + + assert_iter_eq!( + readarr_data.sorted_quality_profile_names(), + expected_quality_profile_vec + ); + } + + #[test] + fn test_sorted_metadata_profile_names() { + let mut metadata_profile_map = BiMap::new(); + metadata_profile_map.insert(3, "test 1".to_owned()); + metadata_profile_map.insert(2, "test 2".to_owned()); + metadata_profile_map.insert(1, "test 3".to_owned()); + let readarr_data = ReadarrData { + metadata_profile_map, + ..ReadarrData::default() + }; + let expected_metadata_profile_vec = vec![ + "test 3".to_owned(), + "test 2".to_owned(), + "test 1".to_owned(), + ]; + + assert_iter_eq!( + readarr_data.sorted_metadata_profile_names(), + expected_metadata_profile_vec + ); + } + + #[test] + fn test_readarr_data_default() { + let readarr_data = ReadarrData::default(); + + assert_none!(readarr_data.add_author_modal); + assert_none!(readarr_data.add_author_search); + assert!(!readarr_data.add_import_list_exclusion); + assert_none!(readarr_data.add_root_folder_modal); + assert_none!(readarr_data.add_searched_authors); + assert_is_empty!(readarr_data.author_history); + assert_is_empty!(readarr_data.author_releases); + assert_is_empty!(readarr_data.authors); + assert_is_empty!(readarr_data.blocklist); + assert_none!(readarr_data.book_details_modal); + assert_is_empty!(readarr_data.books); + assert!(!readarr_data.delete_files); + assert_is_empty!(readarr_data.disk_space_vec); + assert_is_empty!(readarr_data.downloads); + assert_none!(readarr_data.edit_author_modal); + assert_none!(readarr_data.edit_indexer_modal); + assert_is_empty!(readarr_data.history); + assert_none!(readarr_data.indexer_settings); + assert_none!(readarr_data.indexer_test_all_results); + assert_none!(readarr_data.indexer_test_errors); + assert_is_empty!(readarr_data.indexers); + assert_is_empty!(readarr_data.log_details); + assert_is_empty!(readarr_data.logs); + assert_is_empty!(readarr_data.metadata_profile_map); + assert!(!readarr_data.prompt_confirm); + assert_none!(readarr_data.prompt_confirm_action); + assert_is_empty!(readarr_data.quality_profile_map); + assert_is_empty!(readarr_data.queued_events); + assert_is_empty!(readarr_data.root_folders); + assert_eq!(readarr_data.selected_block, BlockSelectionState::default()); + assert_eq!(readarr_data.start_time, >::default()); + assert_is_empty!(readarr_data.tags_map); + assert_is_empty!(readarr_data.tasks); + assert_is_empty!(readarr_data.updates); + assert_is_empty!(readarr_data.version); + + assert_eq!(readarr_data.main_tabs.tabs.len(), 7); + + assert_str_eq!(readarr_data.main_tabs.tabs[0].title, "Library"); + assert_eq!( + readarr_data.main_tabs.tabs[0].route, + ActiveReadarrBlock::Authors.into() + ); + assert_some_eq_x!( + &readarr_data.main_tabs.tabs[0].contextual_help, + &AUTHORS_CONTEXT_CLUES + ); + assert_none!(readarr_data.main_tabs.tabs[0].config); + + assert_str_eq!(readarr_data.main_tabs.tabs[1].title, "Downloads"); + assert_eq!( + readarr_data.main_tabs.tabs[1].route, + ActiveReadarrBlock::Downloads.into() + ); + assert_some_eq_x!( + &readarr_data.main_tabs.tabs[1].contextual_help, + &DOWNLOADS_CONTEXT_CLUES + ); + assert_none!(readarr_data.main_tabs.tabs[1].config); + + assert_str_eq!(readarr_data.main_tabs.tabs[2].title, "Blocklist"); + assert_eq!( + readarr_data.main_tabs.tabs[2].route, + ActiveReadarrBlock::Blocklist.into() + ); + assert_some_eq_x!( + &readarr_data.main_tabs.tabs[2].contextual_help, + &BLOCKLIST_CONTEXT_CLUES + ); + assert_none!(readarr_data.main_tabs.tabs[2].config); + + assert_str_eq!(readarr_data.main_tabs.tabs[3].title, "History"); + assert_eq!( + readarr_data.main_tabs.tabs[3].route, + ActiveReadarrBlock::History.into() + ); + assert_some_eq_x!( + &readarr_data.main_tabs.tabs[3].contextual_help, + &HISTORY_CONTEXT_CLUES + ); + assert_none!(readarr_data.main_tabs.tabs[3].config); + + assert_str_eq!(readarr_data.main_tabs.tabs[4].title, "Root Folders"); + assert_eq!( + readarr_data.main_tabs.tabs[4].route, + ActiveReadarrBlock::RootFolders.into() + ); + assert_some_eq_x!( + &readarr_data.main_tabs.tabs[4].contextual_help, + &ROOT_FOLDERS_CONTEXT_CLUES + ); + assert_none!(readarr_data.main_tabs.tabs[4].config); + + assert_str_eq!(readarr_data.main_tabs.tabs[5].title, "Indexers"); + assert_eq!( + readarr_data.main_tabs.tabs[5].route, + ActiveReadarrBlock::Indexers.into() + ); + assert_some_eq_x!( + &readarr_data.main_tabs.tabs[5].contextual_help, + &INDEXERS_CONTEXT_CLUES + ); + assert_none!(readarr_data.main_tabs.tabs[5].config); + + assert_str_eq!(readarr_data.main_tabs.tabs[6].title, "System"); + assert_eq!( + readarr_data.main_tabs.tabs[6].route, + ActiveReadarrBlock::System.into() + ); + assert_some_eq_x!( + &readarr_data.main_tabs.tabs[6].contextual_help, + &SYSTEM_CONTEXT_CLUES + ); + assert_none!(readarr_data.main_tabs.tabs[6].config); + + assert_eq!(readarr_data.author_info_tabs.tabs.len(), 3); + + assert_str_eq!(readarr_data.author_info_tabs.tabs[0].title, "Books"); + assert_eq!( + readarr_data.author_info_tabs.tabs[0].route, + ActiveReadarrBlock::AuthorDetails.into() + ); + assert_some_eq_x!( + &readarr_data.author_info_tabs.tabs[0].contextual_help, + &AUTHOR_DETAILS_CONTEXT_CLUES + ); + assert_none!(readarr_data.author_info_tabs.tabs[0].config); + + assert_str_eq!(readarr_data.author_info_tabs.tabs[1].title, "History"); + assert_eq!( + readarr_data.author_info_tabs.tabs[1].route, + ActiveReadarrBlock::AuthorHistory.into() + ); + assert_some_eq_x!( + &readarr_data.author_info_tabs.tabs[1].contextual_help, + &AUTHOR_HISTORY_CONTEXT_CLUES + ); + assert_none!(readarr_data.author_info_tabs.tabs[1].config); + + assert_str_eq!(readarr_data.author_info_tabs.tabs[2].title, "Author Search"); + assert_eq!( + readarr_data.author_info_tabs.tabs[2].route, + ActiveReadarrBlock::ManualAuthorSearch.into() + ); + assert_some_eq_x!( + &readarr_data.author_info_tabs.tabs[2].contextual_help, + &MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES + ); + assert_none!(readarr_data.author_info_tabs.tabs[2].config); + } + + #[test] + fn test_library_blocks_contains_expected_blocks() { + assert_eq!(LIBRARY_BLOCKS.len(), 7); + assert!(LIBRARY_BLOCKS.contains(&ActiveReadarrBlock::Authors)); + assert!(LIBRARY_BLOCKS.contains(&ActiveReadarrBlock::AuthorsSortPrompt)); + assert!(LIBRARY_BLOCKS.contains(&ActiveReadarrBlock::FilterAuthors)); + assert!(LIBRARY_BLOCKS.contains(&ActiveReadarrBlock::FilterAuthorsError)); + assert!(LIBRARY_BLOCKS.contains(&ActiveReadarrBlock::SearchAuthors)); + assert!(LIBRARY_BLOCKS.contains(&ActiveReadarrBlock::SearchAuthorsError)); + assert!(LIBRARY_BLOCKS.contains(&ActiveReadarrBlock::UpdateAllAuthorsPrompt)); + } + + #[test] + fn test_author_details_blocks_contains_expected_blocks() { + assert_eq!(AUTHOR_DETAILS_BLOCKS.len(), 15); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::AuthorDetails)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::AuthorHistory)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::AuthorHistoryDetails)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::AuthorHistorySortPrompt)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::AutomaticallySearchAuthorPrompt)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::FilterAuthorHistory)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::FilterAuthorHistoryError)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::ManualAuthorSearch)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::ManualAuthorSearchConfirmPrompt)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::ManualAuthorSearchSortPrompt)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SearchBooks)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SearchBooksError)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SearchAuthorHistory)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SearchAuthorHistoryError)); + assert!(AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::UpdateAndScanAuthorPrompt)); + } + + #[test] + fn test_book_details_blocks_contents() { + assert_eq!(BOOK_DETAILS_BLOCKS.len(), 16); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookDetails)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookHistory)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookHistoryDetails)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookHistorySortPrompt)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::AutomaticallySearchBookPrompt)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::DeleteBookFilePrompt)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::FilterBookHistory)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::FilterBookHistoryError)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookFileInfo)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::ManualBookSearch)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::ManualBookSearchConfirmPrompt)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::ManualBookSearchSortPrompt)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SearchBookHistory)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SearchBookHistoryError)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SearchEditions)); + assert!(BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SearchEditionsError)); + } + + #[test] + fn test_book_details_blocks_excludes_book_edition_details() { + assert!(!BOOK_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookEditionDetails)); + } + + #[test] + fn test_edition_details_blocks_contents() { + assert_eq!(EDITION_DETAILS_BLOCKS.len(), 1); + assert!(EDITION_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookEditionDetails)); + } + + #[test] + fn test_blocklist_blocks_contents() { + assert_eq!(BLOCKLIST_BLOCKS.len(), 5); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveReadarrBlock::Blocklist)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveReadarrBlock::BlocklistItemDetails)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveReadarrBlock::DeleteBlocklistItemPrompt)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveReadarrBlock::BlocklistClearAllItemsPrompt)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveReadarrBlock::BlocklistSortPrompt)); + } + + #[test] + fn test_downloads_blocks_contains_expected_blocks() { + assert_eq!(DOWNLOADS_BLOCKS.len(), 3); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveReadarrBlock::Downloads)); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveReadarrBlock::DeleteDownloadPrompt)); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveReadarrBlock::UpdateDownloadsPrompt)); + } + + #[test] + fn test_history_blocks_contains_expected_blocks() { + assert_eq!(HISTORY_BLOCKS.len(), 7); + assert!(HISTORY_BLOCKS.contains(&ActiveReadarrBlock::History)); + assert!(HISTORY_BLOCKS.contains(&ActiveReadarrBlock::HistoryItemDetails)); + assert!(HISTORY_BLOCKS.contains(&ActiveReadarrBlock::HistorySortPrompt)); + assert!(HISTORY_BLOCKS.contains(&ActiveReadarrBlock::SearchHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveReadarrBlock::SearchHistoryError)); + assert!(HISTORY_BLOCKS.contains(&ActiveReadarrBlock::FilterHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveReadarrBlock::FilterHistoryError)); + } + + #[test] + fn test_add_author_blocks_contents() { + assert_eq!(ADD_AUTHOR_BLOCKS.len(), 12); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorAlreadyInLibrary)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorConfirmPrompt)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorEmptySearchResults)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorPrompt)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorSearchInput)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorSearchResults)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorSelectMetadataProfile)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorSelectMonitor)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorSelectMonitorNewItems)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorSelectQualityProfile)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorSelectRootFolder)); + assert!(ADD_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::AddAuthorTagsInput)); + } + + #[test] + fn test_add_author_selection_blocks_ordering() { + let mut add_author_block_iter = ADD_AUTHOR_SELECTION_BLOCKS.iter(); + + assert_eq!( + add_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddAuthorSelectRootFolder] + ); + assert_eq!( + add_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddAuthorSelectMonitor] + ); + assert_eq!( + add_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddAuthorSelectMonitorNewItems] + ); + assert_eq!( + add_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddAuthorSelectQualityProfile] + ); + assert_eq!( + add_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddAuthorSelectMetadataProfile] + ); + assert_eq!( + add_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddAuthorTagsInput] + ); + assert_eq!( + add_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddAuthorConfirmPrompt] + ); + assert_none!(add_author_block_iter.next()); + } + + #[test] + fn test_delete_author_blocks_contents() { + assert_eq!(DELETE_AUTHOR_BLOCKS.len(), 4); + assert!(DELETE_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::DeleteAuthorPrompt)); + assert!(DELETE_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::DeleteAuthorConfirmPrompt)); + assert!(DELETE_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::DeleteAuthorToggleDeleteFile)); + assert!(DELETE_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion)); + } + + #[test] + fn test_delete_author_selection_blocks_ordering() { + let mut delete_author_block_iter = DELETE_AUTHOR_SELECTION_BLOCKS.iter(); + + assert_eq!( + delete_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::DeleteAuthorToggleDeleteFile] + ); + assert_eq!( + delete_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion] + ); + assert_eq!( + delete_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::DeleteAuthorConfirmPrompt] + ); + assert_none!(delete_author_block_iter.next()); + } + + #[test] + fn test_delete_book_blocks_contents() { + assert_eq!(DELETE_BOOK_BLOCKS.len(), 4); + assert!(DELETE_BOOK_BLOCKS.contains(&ActiveReadarrBlock::DeleteBookPrompt)); + assert!(DELETE_BOOK_BLOCKS.contains(&ActiveReadarrBlock::DeleteBookConfirmPrompt)); + assert!(DELETE_BOOK_BLOCKS.contains(&ActiveReadarrBlock::DeleteBookToggleDeleteFile)); + assert!(DELETE_BOOK_BLOCKS.contains(&ActiveReadarrBlock::DeleteBookToggleAddListExclusion)); + } + + #[test] + fn test_delete_book_selection_blocks_ordering() { + let mut delete_book_block_iter = DELETE_BOOK_SELECTION_BLOCKS.iter(); + + assert_eq!( + delete_book_block_iter.next().unwrap(), + &[ActiveReadarrBlock::DeleteBookToggleDeleteFile] + ); + assert_eq!( + delete_book_block_iter.next().unwrap(), + &[ActiveReadarrBlock::DeleteBookToggleAddListExclusion] + ); + assert_eq!( + delete_book_block_iter.next().unwrap(), + &[ActiveReadarrBlock::DeleteBookConfirmPrompt] + ); + assert_none!(delete_book_block_iter.next()); + } + + #[test] + fn test_edit_author_blocks() { + assert_eq!(EDIT_AUTHOR_BLOCKS.len(), 8); + assert!(EDIT_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::EditAuthorPrompt)); + assert!(EDIT_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::EditAuthorConfirmPrompt)); + assert!(EDIT_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::EditAuthorPathInput)); + assert!(EDIT_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::EditAuthorSelectMetadataProfile)); + assert!(EDIT_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::EditAuthorSelectMonitorNewItems)); + assert!(EDIT_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::EditAuthorSelectQualityProfile)); + assert!(EDIT_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::EditAuthorTagsInput)); + assert!(EDIT_AUTHOR_BLOCKS.contains(&ActiveReadarrBlock::EditAuthorToggleMonitored)); + } + + #[test] + fn test_edit_author_selection_blocks_ordering() { + let mut edit_author_block_iter = EDIT_AUTHOR_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::EditAuthorToggleMonitored] + ); + assert_eq!( + edit_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::EditAuthorSelectMonitorNewItems] + ); + assert_eq!( + edit_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::EditAuthorSelectQualityProfile] + ); + assert_eq!( + edit_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::EditAuthorSelectMetadataProfile] + ); + assert_eq!( + edit_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::EditAuthorPathInput] + ); + assert_eq!( + edit_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::EditAuthorTagsInput] + ); + assert_eq!( + edit_author_block_iter.next().unwrap(), + &[ActiveReadarrBlock::EditAuthorConfirmPrompt] + ); + assert_none!(edit_author_block_iter.next()); + } + + #[test] + fn test_root_folders_blocks_contents() { + assert_eq!(ROOT_FOLDERS_BLOCKS.len(), 2); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveReadarrBlock::RootFolders)); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveReadarrBlock::DeleteRootFolderPrompt)); + } + + #[test] + fn test_add_root_folder_blocks_contents() { + assert_eq!(ADD_ROOT_FOLDER_BLOCKS.len(), 9); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderPrompt)); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderConfirmPrompt)); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderNameInput)); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderPathInput)); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderSelectMonitor)); + assert!( + ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems) + ); + assert!( + ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderSelectQualityProfile) + ); + assert!( + ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderSelectMetadataProfile) + ); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveReadarrBlock::AddRootFolderTagsInput)); + } + + #[test] + fn test_add_root_folder_selection_blocks_ordering() { + let mut add_root_folder_block_iter = ADD_ROOT_FOLDER_SELECTION_BLOCKS.iter(); + + assert_eq!( + add_root_folder_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddRootFolderNameInput] + ); + assert_eq!( + add_root_folder_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddRootFolderPathInput] + ); + assert_eq!( + add_root_folder_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddRootFolderSelectMonitor] + ); + assert_eq!( + add_root_folder_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems] + ); + assert_eq!( + add_root_folder_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddRootFolderSelectQualityProfile] + ); + assert_eq!( + add_root_folder_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddRootFolderSelectMetadataProfile] + ); + assert_eq!( + add_root_folder_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddRootFolderTagsInput] + ); + assert_eq!( + add_root_folder_block_iter.next().unwrap(), + &[ActiveReadarrBlock::AddRootFolderConfirmPrompt] + ); + assert_none!(add_root_folder_block_iter.next()); + } + + #[test] + fn test_edit_indexer_blocks_contents() { + assert_eq!(EDIT_INDEXER_BLOCKS.len(), 11); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerConfirmPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerApiKeyInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerNameInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerSeedRatioInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerToggleEnableRss)); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch) + ); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch) + ); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerPriorityInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerUrlInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveReadarrBlock::EditIndexerTagsInput)); + } + + #[test] + fn test_edit_indexer_torrent_selection_blocks_ordering() { + let mut edit_indexer_block_iter = EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerUrlInput + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerToggleEnableRss, + ActiveReadarrBlock::EditIndexerApiKeyInput + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveReadarrBlock::EditIndexerSeedRatioInput + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveReadarrBlock::EditIndexerTagsInput + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerPriorityInput, + ActiveReadarrBlock::EditIndexerConfirmPrompt + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ActiveReadarrBlock::EditIndexerConfirmPrompt + ] + ); + assert_none!(edit_indexer_block_iter.next()); + } + + #[test] + fn test_edit_indexer_nzb_selection_blocks_ordering() { + let mut edit_indexer_block_iter = EDIT_INDEXER_NZB_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerUrlInput + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerToggleEnableRss, + ActiveReadarrBlock::EditIndexerApiKeyInput + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveReadarrBlock::EditIndexerTagsInput + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveReadarrBlock::EditIndexerPriorityInput + ] + ); + assert_eq!( + edit_indexer_block_iter.next().unwrap(), + &[ + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ActiveReadarrBlock::EditIndexerConfirmPrompt + ] + ); + assert_none!(edit_indexer_block_iter.next()); + } + + #[test] + fn test_indexer_settings_blocks_contents() { + assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 6); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveReadarrBlock::AllIndexerSettingsPrompt)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveReadarrBlock::IndexerSettingsConfirmPrompt)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveReadarrBlock::IndexerSettingsMaximumSizeInput)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveReadarrBlock::IndexerSettingsMinimumAgeInput)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveReadarrBlock::IndexerSettingsRetentionInput)); + assert!( + INDEXER_SETTINGS_BLOCKS.contains(&ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput) + ); + } + + #[test] + fn test_indexer_settings_selection_blocks_ordering() { + let mut indexer_settings_block_iter = INDEXER_SETTINGS_SELECTION_BLOCKS.iter(); + + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveReadarrBlock::IndexerSettingsMinimumAgeInput] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveReadarrBlock::IndexerSettingsRetentionInput] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveReadarrBlock::IndexerSettingsMaximumSizeInput] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveReadarrBlock::IndexerSettingsConfirmPrompt] + ); + assert_none!(indexer_settings_block_iter.next()); + } + + #[test] + fn test_indexers_blocks_contents() { + assert_eq!(INDEXERS_BLOCKS.len(), 3); + assert!(INDEXERS_BLOCKS.contains(&ActiveReadarrBlock::Indexers)); + assert!(INDEXERS_BLOCKS.contains(&ActiveReadarrBlock::DeleteIndexerPrompt)); + assert!(INDEXERS_BLOCKS.contains(&ActiveReadarrBlock::TestIndexer)); + } + + #[test] + fn test_indexers_blocks_excludes_test_all_indexers() { + assert!(!INDEXERS_BLOCKS.contains(&ActiveReadarrBlock::TestAllIndexers)); + } + + #[test] + fn test_system_details_blocks_contents() { + assert_eq!(SYSTEM_DETAILS_BLOCKS.len(), 5); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SystemLogs)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SystemQueuedEvents)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SystemTasks)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SystemTaskStartConfirmPrompt)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::SystemUpdates)); + } +} diff --git a/src/network/mod.rs b/src/network/mod.rs index f74ba3b9..8cf353ca 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -19,12 +19,14 @@ use tokio_util::sync::CancellationToken; use crate::app::{App, ServarrConfig}; use crate::models::Serdeable; use crate::network::radarr_network::RadarrEvent; +use crate::network::readarr_network::ReadarrEvent; #[cfg(test)] use mockall::automock; use reqwest::header::HeaderMap; pub mod lidarr_network; pub mod radarr_network; +pub mod readarr_network; pub mod sonarr_network; mod utils; @@ -50,6 +52,7 @@ pub enum NetworkEvent { Radarr(RadarrEvent), Sonarr(SonarrEvent), Lidarr(LidarrEvent), + Readarr(ReadarrEvent), } #[derive(Clone)] @@ -75,6 +78,10 @@ impl NetworkTrait for Network<'_, '_> { .handle_lidarr_event(lidarr_event) .await .map(Serdeable::from), + NetworkEvent::Readarr(readarr_event) => self + .handle_readarr_event(readarr_event) + .await + .map(Serdeable::from), }; let mut app = self.app.lock().await; @@ -242,6 +249,7 @@ impl<'a, 'b> Network<'a, 'b> { NetworkEvent::Radarr(_) => (7878, "v3"), NetworkEvent::Sonarr(_) => (8989, "v3"), NetworkEvent::Lidarr(_) => (8686, "v1"), + NetworkEvent::Readarr(_) => (8787, "v1"), }; let mut uri = if let Some(servarr_uri) = uri { format!("{servarr_uri}/api/{api_version}{resource}") diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index 2b14efc7..7672144d 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -944,6 +944,7 @@ pub(in crate::network) mod test_utils { let network_event_clone: NetworkEvent = network_event.clone().into(); let api_version = match &network_event_clone { NetworkEvent::Lidarr(_) => "v1", + NetworkEvent::Readarr(_) => "v1", _ => "v3", }; let mut server = Server::new_async().await; diff --git a/src/network/readarr_network/mod.rs b/src/network/readarr_network/mod.rs new file mode 100644 index 00000000..747f5a65 --- /dev/null +++ b/src/network/readarr_network/mod.rs @@ -0,0 +1,29 @@ +use anyhow::Result; + +use super::{NetworkEvent, NetworkResource}; +use crate::models::readarr_models::ReadarrSerdeable; +use crate::network::Network; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum ReadarrEvent {} + +impl NetworkResource for ReadarrEvent { + fn resource(&self) -> &'static str { + match *self {} + } +} + +impl From for NetworkEvent { + fn from(readarr_event: ReadarrEvent) -> Self { + NetworkEvent::Readarr(readarr_event) + } +} + +impl Network<'_, '_> { + pub async fn handle_readarr_event( + &mut self, + readarr_event: ReadarrEvent, + ) -> Result { + match readarr_event {} + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 17bc5a77..db2c3a93 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,6 +10,7 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::Tabs; use ratatui::widgets::Wrap; use ratatui::widgets::{Clear, Row}; +use readarr_ui::ReadarrUi; use sonarr_ui::SonarrUi; use utils::layout_block; @@ -32,6 +33,7 @@ use crate::ui::widgets::popup::{Popup, Size}; mod builtin_themes; mod lidarr_ui; mod radarr_ui; +mod readarr_ui; mod sonarr_ui; mod styles; pub mod theme; @@ -94,6 +96,10 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { LidarrUi::draw_context_row(f, app, context_area); LidarrUi::draw(f, app, table_area); } + route if ReadarrUi::accepts(route) => { + ReadarrUi::draw_context_row(f, app, context_area); + ReadarrUi::draw(f, app, table_area); + } _ => (), } diff --git a/src/ui/readarr_ui/library/library_ui_tests.rs b/src/ui/readarr_ui/library/library_ui_tests.rs new file mode 100644 index 00000000..65f7e839 --- /dev/null +++ b/src/ui/readarr_ui/library/library_ui_tests.rs @@ -0,0 +1,235 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::readarr_models::{Author, AuthorStatistics, AuthorStatus}; + use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, LIBRARY_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::readarr_ui::library::{LibraryUi, decorate_author_row_with_style}; + use crate::ui::styles::ManagarrStyle; + use pretty_assertions::assert_eq; + use ratatui::widgets::{Cell, Row}; + + #[test] + fn test_library_ui_accepts() { + for active_readarr_block in ActiveReadarrBlock::iter() { + if LIBRARY_BLOCKS.contains(&active_readarr_block) { + assert!( + LibraryUi::accepts(active_readarr_block.into()), + "{active_readarr_block} is not accepted by the LibraryUi" + ); + } else { + assert!( + !LibraryUi::accepts(active_readarr_block.into()), + "{active_readarr_block} should not be accepted by LibraryUi" + ); + } + } + } + + #[test] + fn test_decorate_row_with_style_unmonitored() { + let author = Author::default(); + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.unmonitored()); + } + + #[test] + fn test_decorate_row_with_style_downloaded_when_ended_and_all_books_present() { + let author = Author { + monitored: true, + status: AuthorStatus::Ended, + statistics: Some(AuthorStatistics { + book_file_count: 10, + total_book_count: 10, + ..AuthorStatistics::default() + }), + ..Author::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.downloaded()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_ended_and_books_are_missing() { + let author = Author { + monitored: true, + status: AuthorStatus::Ended, + statistics: Some(AuthorStatistics { + book_file_count: 5, + total_book_count: 10, + ..AuthorStatistics::default() + }), + ..Author::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_ended_and_no_statistics() { + let author = Author { + monitored: true, + status: AuthorStatus::Ended, + statistics: None, + ..Author::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_ended_and_total_book_count_is_zero() { + let author = Author { + monitored: true, + status: AuthorStatus::Ended, + statistics: Some(AuthorStatistics { + book_file_count: 0, + total_book_count: 0, + ..AuthorStatistics::default() + }), + ..Author::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_unreleased_when_continuing_and_all_books_present() { + let author = Author { + monitored: true, + status: AuthorStatus::Continuing, + statistics: Some(AuthorStatistics { + book_file_count: 10, + total_book_count: 10, + ..AuthorStatistics::default() + }), + ..Author::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.unreleased()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_continuing_and_books_are_missing() { + let author = Author { + monitored: true, + status: AuthorStatus::Continuing, + statistics: Some(AuthorStatistics { + book_file_count: 5, + total_book_count: 10, + ..AuthorStatistics::default() + }), + ..Author::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_continuing_and_no_statistics() { + let author = Author { + monitored: true, + status: AuthorStatus::Continuing, + statistics: None, + ..Author::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_defaults_to_indeterminate_for_deleted_status() { + let author = Author { + monitored: true, + status: AuthorStatus::Deleted, + ..Author::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_author_row_with_style(&author, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + mod snapshot_tests { + use crate::app::App; + use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; + use rstest::rstest; + + use crate::ui::DrawUi; + use crate::ui::readarr_ui::library::LibraryUi; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + + #[rstest] + fn test_library_ui_renders( + #[values( + ActiveReadarrBlock::Authors, + ActiveReadarrBlock::AuthorsSortPrompt, + ActiveReadarrBlock::SearchAuthors, + ActiveReadarrBlock::SearchAuthorsError, + ActiveReadarrBlock::FilterAuthors, + ActiveReadarrBlock::FilterAuthorsError + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_readarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("readarr_library_{active_readarr_block}"), output); + } + + #[test] + fn test_library_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveReadarrBlock::Authors.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_library_ui_renders_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Authors.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/readarr_ui/library/mod.rs b/src/ui/readarr_ui/library/mod.rs new file mode 100644 index 00000000..1e1730f4 --- /dev/null +++ b/src/ui/readarr_ui/library/mod.rs @@ -0,0 +1,176 @@ +use ratatui::{ + Frame, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, +}; + +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::utils::convert_to_gb; +use crate::{ + app::App, + models::{ + Route, + readarr_models::{Author, AuthorStatus}, + servarr_data::readarr::readarr_data::{ActiveReadarrBlock, LIBRARY_BLOCKS}, + }, + ui::{ + DrawUi, + styles::ManagarrStyle, + utils::{get_width_from_percentage, layout_block_top_border}, + }, +}; + +#[cfg(test)] +#[path = "library_ui_tests.rs"] +mod library_ui_tests; + +pub(super) struct LibraryUi; + +impl DrawUi for LibraryUi { + fn accepts(route: Route) -> bool { + if let Route::Readarr(active_readarr_block, _) = route { + return LIBRARY_BLOCKS.contains(&active_readarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + draw_library(f, app, area); + } +} + +fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Readarr(active_readarr_block, _) = app.get_current_route() { + let current_selection = if !app.data.readarr_data.authors.items.is_empty() { + app.data.readarr_data.authors.current_selection().clone() + } else { + Author::default() + }; + let quality_profile_map = &app.data.readarr_data.quality_profile_map; + let metadata_profile_map = &app.data.readarr_data.metadata_profile_map; + let tags_map = &app.data.readarr_data.tags_map; + let content = Some(&mut app.data.readarr_data.authors); + + let authors_table_row_mapping = |author: &Author| { + author.author_name.scroll_left_or_reset( + get_width_from_percentage(area, 25), + *author == current_selection, + app.should_text_scroll, + ); + let monitored = if author.monitored { "🏷" } else { "" }; + let author_type = author.author_type.clone().unwrap_or_default(); + let size = author + .statistics + .as_ref() + .map_or(0f64, |stats| convert_to_gb(stats.size_on_disk)); + let quality_profile = quality_profile_map + .get_by_left(&author.quality_profile_id) + .cloned() + .unwrap_or_default(); + let metadata_profile = metadata_profile_map + .get_by_left(&author.metadata_profile_id) + .cloned() + .unwrap_or_default(); + let books = author + .statistics + .as_ref() + .map_or(0, |stats| stats.book_count); + let tags = author + .tags + .iter() + .filter_map(|tag_id| { + let id = tag_id.as_i64()?; + tags_map.get_by_left(&id).cloned() + }) + .collect::>() + .join(", "); + + decorate_author_row_with_style( + author, + Row::new(vec![ + Cell::from(author.author_name.to_string()), + Cell::from(author_type), + Cell::from(author.status.to_display_str()), + Cell::from(quality_profile), + Cell::from(metadata_profile), + Cell::from(books.to_string()), + Cell::from(format!("{size:.2} GB")), + Cell::from(monitored.to_owned()), + Cell::from(tags), + ]), + ) + }; + let authors_table = ManagarrTable::new(content, authors_table_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_readarr_block == ActiveReadarrBlock::AuthorsSortPrompt) + .searching(active_readarr_block == ActiveReadarrBlock::SearchAuthors) + .filtering(active_readarr_block == ActiveReadarrBlock::FilterAuthors) + .search_produced_empty_results(active_readarr_block == ActiveReadarrBlock::SearchAuthorsError) + .filter_produced_empty_results(active_readarr_block == ActiveReadarrBlock::FilterAuthorsError) + .headers([ + "Name", + "Type", + "Status", + "Quality Profile", + "Metadata Profile", + "Books", + "Size", + "Monitored", + "Tags", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(9), + Constraint::Percentage(8), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(7), + Constraint::Percentage(8), + Constraint::Percentage(6), + Constraint::Percentage(11), + ]); + + if [ + ActiveReadarrBlock::SearchAuthors, + ActiveReadarrBlock::FilterAuthors, + ] + .contains(&active_readarr_block) + { + authors_table.show_cursor(f, area); + } + + f.render_widget(authors_table, area); + } +} + +fn decorate_author_row_with_style<'a>(author: &Author, row: Row<'a>) -> Row<'a> { + if !author.monitored { + return row.unmonitored(); + } + + match author.status { + AuthorStatus::Ended => { + if let Some(ref stats) = author.statistics { + return if stats.book_file_count == stats.total_book_count && stats.total_book_count > 0 { + row.downloaded() + } else { + row.missing() + }; + } + row.indeterminate() + } + AuthorStatus::Continuing => { + if let Some(ref stats) = author.statistics { + return if stats.book_file_count == stats.total_book_count && stats.total_book_count > 0 { + row.unreleased() + } else { + row.missing() + }; + } + row.indeterminate() + } + _ => row.indeterminate(), + } +} diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap new file mode 100644 index 00000000..fead8278 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/readarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap new file mode 100644 index 00000000..90000a03 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/readarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_Authors.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_Authors.snap new file mode 100644 index 00000000..75b980f2 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_Authors.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/readarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Books Size Monitored Tags +=> Test Author Continuing Standard Standard 0 0.00 GB 🏷 alex diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_AuthorsSortPrompt.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_AuthorsSortPrompt.snap new file mode 100644 index 00000000..4a9fb66d --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_AuthorsSortPrompt.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/readarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name Type Status Quality Profile Metadata Profile Books Size Monitored Tags +=> Test Author Continuing Standard Standard 0 0.00 GB 🏷 alex + + + + + + + + + + + ╭───────────────────────────────╮ + │Name │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────╯ diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_FilterAuthors.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_FilterAuthors.snap new file mode 100644 index 00000000..e45676e5 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_FilterAuthors.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/readarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Books Size Monitored Tags +=> Test Author Continuing Standard Standard 0 0.00 GB 🏷 alex + + + + + + + + + + + + + + + + + + + ╭───────────────── Filter ──────────────────╮ + │author filter │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_FilterAuthorsError.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_FilterAuthorsError.snap new file mode 100644 index 00000000..7380fa4f --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_FilterAuthorsError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/readarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Books Size Monitored Tags +=> Test Author Continuing Standard Standard 0 0.00 GB 🏷 alex + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │The given filter produced empty results│ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_SearchAuthors.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_SearchAuthors.snap new file mode 100644 index 00000000..8fa53ca2 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_SearchAuthors.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/readarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Books Size Monitored Tags +=> Test Author Continuing Standard Standard 0 0.00 GB 🏷 alex + + + + + + + + + + + + + + + + + + + ╭───────────────── Search ──────────────────╮ + │author search │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_SearchAuthorsError.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_SearchAuthorsError.snap new file mode 100644 index 00000000..7a519870 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__library_ui_tests__tests__snapshot_tests__readarr_library_SearchAuthorsError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/readarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Books Size Monitored Tags +=> Test Author Continuing Standard Standard 0 0.00 GB 🏷 alex + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │ No items found matching search │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/mod.rs b/src/ui/readarr_ui/mod.rs new file mode 100644 index 00000000..643b9cce --- /dev/null +++ b/src/ui/readarr_ui/mod.rs @@ -0,0 +1,217 @@ +#[cfg(test)] +use crate::ui::ui_test_utils::test_utils::Utc; +use chrono::Duration; +#[cfg(not(test))] +use chrono::Utc; +use library::LibraryUi; +use ratatui::{ + Frame, + layout::{Constraint, Layout, Rect}, + style::Stylize, + text::Text, + widgets::Paragraph, +}; +use std::{cmp, iter}; + +use super::{ + DrawUi, draw_tabs, + styles::ManagarrStyle, + utils::{ + borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block, + }, + widgets::loading_block::LoadingBlock, +}; +use crate::ui::utils::{extract_monitored_disk_space_vec, extract_monitored_root_folders}; +use crate::{ + app::App, + logos::READARR_LOGO, + models::{ + Route, + readarr_models::DownloadRecord, + servarr_data::readarr::readarr_data::ReadarrData, + servarr_models::{DiskSpace, RootFolder}, + }, + utils::convert_to_gb, +}; + +mod library; + +#[cfg(test)] +#[path = "readarr_ui_tests.rs"] +mod readarr_ui_tests; + +pub(super) struct ReadarrUi; + +impl DrawUi for ReadarrUi { + fn accepts(route: Route) -> bool { + matches!(route, Route::Readarr(_, _)) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let content_area = draw_tabs(f, area, "Authors", &app.data.readarr_data.main_tabs); + let route = app.get_current_route(); + + match route { + _ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area), + _ => (), + } + } + + fn draw_context_row(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let [main_area, logo_area] = + Layout::horizontal([Constraint::Fill(0), Constraint::Length(20)]).areas(area); + + let [stats_area, downloads_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(main_area); + + draw_stats_context(f, app, stats_area); + draw_downloads_context(f, app, downloads_area); + draw_readarr_logo(f, logo_area); + } +} + +fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let block = title_block("Stats"); + + if !app.data.readarr_data.version.is_empty() { + f.render_widget(block, area); + let ReadarrData { + root_folders, + disk_space_vec, + start_time, + .. + } = &app.data.readarr_data; + let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone()); + let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone()); + + let mut constraints = vec![ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ]; + + constraints.append( + &mut iter::repeat_n( + Constraint::Length(1), + monitored_disk_space_vec.len() + monitored_root_folders.len() + 1, + ) + .collect(), + ); + + let stat_item_areas = Layout::vertical(constraints).margin(1).split(area); + + let version_paragraph = Paragraph::new(Text::from(format!( + "Readarr Version: {}", + app.data.readarr_data.version + ))) + .block(borderless_block()) + .bold(); + + let uptime = Utc::now() - start_time.to_owned(); + let days = uptime.num_days(); + let day_difference = uptime - Duration::days(days); + let hours = day_difference.num_hours(); + let hour_difference = day_difference - Duration::hours(hours); + let minutes = hour_difference.num_minutes(); + let seconds = (hour_difference - Duration::minutes(minutes)).num_seconds(); + + let uptime_paragraph = Paragraph::new(Text::from(format!( + "Uptime: {days}d {hours:0width$}:{minutes:0width$}:{seconds:0width$}", + width = 2 + ))) + .block(borderless_block()) + .bold(); + + let storage = Paragraph::new(Text::from("Storage:")).block(borderless_block().bold()); + let folders = Paragraph::new(Text::from("Root Folders:")).block(borderless_block().bold()); + + f.render_widget(version_paragraph, stat_item_areas[0]); + f.render_widget(uptime_paragraph, stat_item_areas[1]); + f.render_widget(storage, stat_item_areas[2]); + + for i in 0..monitored_disk_space_vec.len() { + let DiskSpace { + path, + free_space, + total_space, + } = &monitored_disk_space_vec[i]; + let title = if let Some(path) = path { + path + } else { + &format!("Disk {}", i + 1) + }; + let ratio = if *total_space == 0 { + 0f64 + } else { + 1f64 - (*free_space as f64 / *total_space as f64) + }; + + let space_gauge = line_gauge_with_label(title.as_str(), ratio); + + f.render_widget(space_gauge, stat_item_areas[i + 3]); + } + + f.render_widget(folders, stat_item_areas[monitored_disk_space_vec.len() + 3]); + + for i in 0..monitored_root_folders.len() { + let RootFolder { + path, free_space, .. + } = &monitored_root_folders[i]; + let space: f64 = convert_to_gb(*free_space); + let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) + .block(borderless_block()) + .default_color(); + + f.render_widget( + root_folder_space, + stat_item_areas[i + monitored_disk_space_vec.len() + 4], + ) + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} + +fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let block = title_block("Downloads"); + let downloads_vec = &app.data.readarr_data.downloads.items; + + if !downloads_vec.is_empty() { + f.render_widget(block, area); + + let max_items = ((((area.height as f64 / 2.0).floor() * 2.0) as i64) / 2) - 1; + let items = cmp::min(downloads_vec.len(), max_items.unsigned_abs() as usize); + let download_item_areas = + Layout::vertical(iter::repeat_n(Constraint::Length(2), items).collect::>()) + .margin(1) + .split(area); + + for i in 0..items { + let DownloadRecord { + title, + sizeleft, + size, + .. + } = &downloads_vec[i]; + let percent = if *size == 0.0 { + 0.0 + } else { + 1f64 - (*sizeleft / *size) + }; + let download_gauge = line_gauge_with_title(title, percent); + + f.render_widget(download_gauge, download_item_areas[i]); + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} + +fn draw_readarr_logo(f: &mut Frame<'_>, area: Rect) { + let logo_text = Text::from(READARR_LOGO); + let logo = Paragraph::new(logo_text) + .light_magenta() + .block(layout_block().default_color()) + .centered(); + f.render_widget(logo, area); +} diff --git a/src/ui/readarr_ui/readarr_ui_tests.rs b/src/ui/readarr_ui/readarr_ui_tests.rs new file mode 100644 index 00000000..57caeea3 --- /dev/null +++ b/src/ui/readarr_ui/readarr_ui_tests.rs @@ -0,0 +1,46 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::Route; + use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; + use crate::ui::DrawUi; + use crate::ui::readarr_ui::ReadarrUi; + + #[test] + fn test_readarr_ui_accepts() { + for readarr_block in ActiveReadarrBlock::iter() { + assert!(ReadarrUi::accepts(Route::Readarr(readarr_block, None))); + } + } + + mod snapshot_tests { + use super::*; + use crate::app::App; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + use rstest::rstest; + + #[rstest] + #[case(ActiveReadarrBlock::Authors, 0)] + #[case(ActiveReadarrBlock::Downloads, 1)] + #[case(ActiveReadarrBlock::Blocklist, 2)] + #[case(ActiveReadarrBlock::History, 3)] + #[case(ActiveReadarrBlock::RootFolders, 4)] + #[case(ActiveReadarrBlock::Indexers, 5)] + #[case(ActiveReadarrBlock::System, 6)] + fn test_readarr_ui_renders_readarr_tabs( + #[case] active_readarr_block: ActiveReadarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_readarr_block.into()); + app.data.readarr_data.main_tabs.set_index(index); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + ReadarrUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("readarr_tabs_{active_readarr_block}"), output); + } + } +} diff --git a/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Authors.snap b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Authors.snap new file mode 100644 index 00000000..62866bc1 --- /dev/null +++ b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Authors.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/readarr_ui/readarr_ui_tests.rs +expression: output +--- +╭ Authors ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Name ▼ Type Status Quality Profile Metadata Profile Books Size Monitored Tags │ +│=> Test Author Continuing Standard Standard 0 0.00 GB 🏷 alex │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Blocklist.snap b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Blocklist.snap new file mode 100644 index 00000000..d3f01721 --- /dev/null +++ b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Blocklist.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/readarr_ui/readarr_ui_tests.rs +expression: output +--- +╭ Authors ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Downloads.snap b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Downloads.snap new file mode 100644 index 00000000..d3f01721 --- /dev/null +++ b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Downloads.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/readarr_ui/readarr_ui_tests.rs +expression: output +--- +╭ Authors ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_History.snap b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_History.snap new file mode 100644 index 00000000..d3f01721 --- /dev/null +++ b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_History.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/readarr_ui/readarr_ui_tests.rs +expression: output +--- +╭ Authors ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Indexers.snap b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Indexers.snap new file mode 100644 index 00000000..d3f01721 --- /dev/null +++ b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_Indexers.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/readarr_ui/readarr_ui_tests.rs +expression: output +--- +╭ Authors ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_RootFolders.snap b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_RootFolders.snap new file mode 100644 index 00000000..d3f01721 --- /dev/null +++ b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_RootFolders.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/readarr_ui/readarr_ui_tests.rs +expression: output +--- +╭ Authors ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_System.snap b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_System.snap new file mode 100644 index 00000000..d3f01721 --- /dev/null +++ b/src/ui/readarr_ui/snapshots/managarr__ui__readarr_ui__readarr_ui_tests__tests__snapshot_tests__readarr_tabs_System.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/readarr_ui/readarr_ui_tests.rs +expression: output +--- +╭ Authors ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab.snap index 1e66720a..e7ffe6b2 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Lidarr Version: 1.2.3.4 ││Test download title ││ ⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_error_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_error_popup.snap index 439c7410..23f6c68a 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_error_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_error_popup.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Lidarr Version: 1.2.3.4 ╭ Keybindings ──────────────────────────────────────────────────────────────────────────╮ ││ ⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_with_error.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_with_error.snap index 007e357d..1b2b382b 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_with_error.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_with_error.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Error | to close ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │Some error │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_notification_success_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_notification_success_popup.snap index 201b8a9f..cb483c72 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_notification_success_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_notification_success_popup.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Lidarr Version: 1.2.3.4 ││Test download title ││ ⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap index 94ea3318..90709a10 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Radarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap index 321fb196..1238a5d7 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Radarr Version: 1.2.3.4 ╭ Keybindings ──────────────────────────────────────────────────────────────────────────╮ ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap index 12f05f7d..53dd902a 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Error | to close ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │Some error │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_failure_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_failure_popup.snap index ce9f2c0c..21403c2d 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_failure_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_failure_popup.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Radarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_success_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_success_popup.snap index ed22e921..84554a8a 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_success_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_success_popup.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Radarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab.snap index 987742eb..187aaf8c 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Sonarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣶⣦⣄⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_error_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_error_popup.snap index 4bbce1e3..6a52d517 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_error_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_error_popup.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Sonarr Version: 1.2.3.4 ╭ Keybindings ──────────────────────────────────────────────────────────────────────────╮ ││ ⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣶⣦⣄⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_with_error.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_with_error.snap index ffbf3868..e839c02a 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_with_error.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_with_error.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Error | to close ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │Some error │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_notification_success_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_notification_success_popup.snap index 7cd77bf0..3b7e7de1 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_notification_success_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_notification_success_popup.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr │ Lidarr to open help│ +│ Radarr │ Sonarr │ Lidarr │ Readarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Sonarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣶⣦⣄⠀⠀⠀ │ diff --git a/src/utils.rs b/src/utils.rs index 788a6a78..33a05929 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -365,6 +365,13 @@ pub fn select_cli_configuration( config.lidarr.as_ref().expect("Lidarr config must exist")[0].clone(); app.server_tabs.select_tab_by_config(&default_lidarr_config); } + Command::Readarr(_) => { + let default_readarr_config = + config.readarr.as_ref().expect("Readarr config must exist")[0].clone(); + app + .server_tabs + .select_tab_by_config(&default_readarr_config); + } _ => (), } }