diff --git a/src/app/context_clues.rs b/src/app/context_clues.rs index f960177..d7ec1b2 100644 --- a/src/app/context_clues.rs +++ b/src/app/context_clues.rs @@ -124,3 +124,8 @@ pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [ DEFAULT_KEYBINDINGS.refresh.desc, ), ]; + +pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ + (DEFAULT_KEYBINDINGS.submit, "start task"), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; diff --git a/src/app/context_clues_tests.rs b/src/app/context_clues_tests.rs index 9e71030..a10881e 100644 --- a/src/app/context_clues_tests.rs +++ b/src/app/context_clues_tests.rs @@ -4,7 +4,7 @@ mod test { BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, - ServarrContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, ServarrContextClueProvider, }; use crate::app::{App, key_binding::DEFAULT_KEYBINDINGS}; use crate::models::servarr_data::ActiveKeybindingBlock; @@ -268,6 +268,21 @@ mod test { assert_none!(system_context_clues_iter.next()); } + #[test] + fn test_system_tasks_context_clues() { + let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + system_tasks_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "start task") + ); + assert_some_eq_x!( + system_tasks_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(system_tasks_context_clues_iter.next()); + } + #[test] fn test_servarr_context_clue_provider_delegates_to_radarr_provider() { let mut app = App::test_default(); @@ -275,10 +290,7 @@ mod test { let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); - assert_some_eq_x!( - context_clues, - &crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES, - ); + assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,); } #[test] @@ -288,10 +300,7 @@ mod test { let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); - assert_some_eq_x!( - context_clues, - &crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES, - ); + assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,); } #[test] diff --git a/src/app/lidarr/lidarr_context_clues.rs b/src/app/lidarr/lidarr_context_clues.rs index ba9abd6..df650d8 100644 --- a/src/app/lidarr/lidarr_context_clues.rs +++ b/src/app/lidarr/lidarr_context_clues.rs @@ -1,6 +1,7 @@ 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; @@ -73,7 +74,9 @@ impl ContextClueProvider for LidarrContextClueProvider { .get_active_route_contextual_help(), ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults - | ActiveLidarrBlock::TestAllIndexers => Some(&BARE_POPUP_CONTEXT_CLUES), + | ActiveLidarrBlock::TestAllIndexers + | ActiveLidarrBlock::SystemLogs + | ActiveLidarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES), _ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) || EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) || INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) @@ -92,6 +95,7 @@ impl ContextClueProvider for LidarrContextClueProvider { _ if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) => { Some(&ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES) } + ActiveLidarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES), _ => app .data .lidarr_data diff --git a/src/app/lidarr/lidarr_context_clues_tests.rs b/src/app/lidarr/lidarr_context_clues_tests.rs index 2c7704b..2e1d703 100644 --- a/src/app/lidarr/lidarr_context_clues_tests.rs +++ b/src/app/lidarr/lidarr_context_clues_tests.rs @@ -3,6 +3,7 @@ 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::lidarr::lidarr_context_clues::{ @@ -266,4 +267,14 @@ mod tests { assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES); } + + #[test] + fn test_sonarr_context_clue_provider_system_tasks_clues() { + let mut app = App::test_default(); + + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES); + } } diff --git a/src/app/lidarr/lidarr_tests.rs b/src/app/lidarr/lidarr_tests.rs index 1c4cd4a..2d082f3 100644 --- a/src/app/lidarr/lidarr_tests.rs +++ b/src/app/lidarr/lidarr_tests.rs @@ -210,6 +210,45 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_system_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::System) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTasks.into()); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQueuedEvents.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetLogs(500).into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_system_updates_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::SystemUpdates) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetUpdates.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_check_for_lidarr_prompt_action_no_prompt_confirm() { let mut app = App::test_default(); diff --git a/src/app/lidarr/mod.rs b/src/app/lidarr/mod.rs index b4522c3..1e4f7b6 100644 --- a/src/app/lidarr/mod.rs +++ b/src/app/lidarr/mod.rs @@ -80,6 +80,22 @@ impl App<'_> { .dispatch_network_event(LidarrEvent::TestAllIndexers.into()) .await; } + ActiveLidarrBlock::System => { + self + .dispatch_network_event(LidarrEvent::GetTasks.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetQueuedEvents.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetLogs(500).into()) + .await; + } + ActiveLidarrBlock::SystemUpdates => { + self + .dispatch_network_event(LidarrEvent::GetUpdates.into()) + .await; + } _ => (), } diff --git a/src/app/radarr/radarr_context_clues.rs b/src/app/radarr/radarr_context_clues.rs index 04f17aa..3222187 100644 --- a/src/app/radarr/radarr_context_clues.rs +++ b/src/app/radarr/radarr_context_clues.rs @@ -1,6 +1,7 @@ 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; @@ -82,11 +83,6 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [ (DEFAULT_KEYBINDINGS.esc, "edit search"), ]; -pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ - (DEFAULT_KEYBINDINGS.submit, "start task"), - (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), -]; - pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ (DEFAULT_KEYBINDINGS.submit, "show overview/add movie"), (DEFAULT_KEYBINDINGS.edit, "edit collection"), diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index 2306ce8..a4b4f10 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -5,12 +5,13 @@ mod tests { BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + SYSTEM_TASKS_CONTEXT_CLUES, }; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::radarr_context_clues::{ ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, - MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider, SYSTEM_TASKS_CONTEXT_CLUES, + MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider, }; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; diff --git a/src/app/sonarr/sonarr_context_clues.rs b/src/app/sonarr/sonarr_context_clues.rs index 20a3bf9..90d8aad 100644 --- a/src/app/sonarr/sonarr_context_clues.rs +++ b/src/app/sonarr/sonarr_context_clues.rs @@ -1,5 +1,6 @@ use crate::app::context_clues::{ BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, }; use crate::app::{App, context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS}; use crate::models::Route; @@ -163,11 +164,6 @@ pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [ (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; -pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ - (DEFAULT_KEYBINDINGS.submit, "start task"), - (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), -]; - pub(in crate::app) struct SonarrContextClueProvider; impl ContextClueProvider for SonarrContextClueProvider { diff --git a/src/app/sonarr/sonarr_context_clues_tests.rs b/src/app/sonarr/sonarr_context_clues_tests.rs index 59a3586..4f02fc3 100644 --- a/src/app/sonarr/sonarr_context_clues_tests.rs +++ b/src/app/sonarr/sonarr_context_clues_tests.rs @@ -4,6 +4,7 @@ mod tests { BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + SYSTEM_TASKS_CONTEXT_CLUES, }; use crate::app::sonarr::sonarr_context_clues::{ SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, SonarrContextClueProvider, @@ -15,7 +16,7 @@ mod tests { ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, - SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, + SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, }, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; diff --git a/src/cli/lidarr/lidarr_command_tests.rs b/src/cli/lidarr/lidarr_command_tests.rs index dc7133f..bf52288 100644 --- a/src/cli/lidarr/lidarr_command_tests.rs +++ b/src/cli/lidarr/lidarr_command_tests.rs @@ -134,6 +134,44 @@ mod tests { assert_ok!(&result); } + #[test] + fn test_start_task_requires_task_name() { + let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "start-task"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_start_task_task_name_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "start-task", + "--task-name", + "test", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_start_task_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "start-task", + "--task-name", + "application-update-check", + ]); + + assert_ok!(&result); + } + #[test] fn test_mark_history_item_as_failed_requires_history_item_id() { let result = @@ -196,6 +234,7 @@ mod tests { use crate::cli::lidarr::get_command_handler::LidarrGetCommand; use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand; use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand; + use crate::models::lidarr_models::LidarrTaskName; use crate::models::servarr_models::IndexerSettings; use crate::{ app::App, @@ -484,6 +523,33 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_start_task_command() { + let expected_task_name = LidarrTaskName::ApplicationUpdateCheck; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::StartTask(expected_task_name).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let start_task_command = LidarrCommand::StartTask { + task_name: LidarrTaskName::ApplicationUpdateCheck, + }; + + let result = LidarrCliHandler::with(&app_arc, start_task_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_test_indexer_command() { let expected_indexer_id = 1; diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs index b072c11..9740ea1 100644 --- a/src/cli/lidarr/list_command_handler.rs +++ b/src/cli/lidarr/list_command_handler.rs @@ -41,14 +41,30 @@ pub enum LidarrListCommand { }, #[command(about = "List all Lidarr indexers")] Indexers, + #[command(about = "Fetch Lidarr logs")] + Logs { + #[arg(long, help = "How many log events to fetch", default_value_t = 500)] + events: u64, + #[arg( + long, + help = "Output the logs in the same format as they appear in the log files" + )] + output_in_log_format: bool, + }, #[command(about = "List all Lidarr metadata profiles")] MetadataProfiles, #[command(about = "List all Lidarr quality profiles")] QualityProfiles, + #[command(about = "List all queued events")] + QueuedEvents, #[command(about = "List all root folders in Lidarr")] RootFolders, #[command(about = "List all Lidarr tags")] Tags, + #[command(about = "List all Lidarr tasks")] + Tasks, + #[command(about = "List all Lidarr updates")] + Updates, } impl From for Command { @@ -58,7 +74,7 @@ impl From for Command { } pub(super) struct LidarrListCommandHandler<'a, 'b> { - _app: &'a Arc>>, + app: &'a Arc>>, command: LidarrListCommand, network: &'a mut dyn NetworkTrait, } @@ -70,7 +86,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH network: &'a mut dyn NetworkTrait, ) -> Self { LidarrListCommandHandler { - _app: app, + app, command, network, } @@ -113,6 +129,23 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + LidarrListCommand::Logs { + events, + output_in_log_format, + } => { + let logs = self + .network + .handle_network_event(LidarrEvent::GetLogs(events).into()) + .await?; + + if output_in_log_format { + let log_lines = &self.app.lock().await.data.sonarr_data.logs.items; + + serde_json::to_string_pretty(log_lines)? + } else { + serde_json::to_string_pretty(&logs)? + } + } LidarrListCommand::MetadataProfiles => { let resp = self .network @@ -127,6 +160,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + LidarrListCommand::QueuedEvents => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetQueuedEvents.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrListCommand::RootFolders => { let resp = self .network @@ -141,6 +181,20 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + LidarrListCommand::Tasks => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetTasks.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Updates => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetUpdates.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } }; Ok(result) diff --git a/src/cli/lidarr/list_command_handler_tests.rs b/src/cli/lidarr/list_command_handler_tests.rs index e18a95d..35048bf 100644 --- a/src/cli/lidarr/list_command_handler_tests.rs +++ b/src/cli/lidarr/list_command_handler_tests.rs @@ -30,7 +30,10 @@ mod tests { "indexers", "metadata-profiles", "quality-profiles", + "queued-events", "tags", + "tasks", + "updates", "root-folders" )] subcommand: &str, @@ -111,6 +114,31 @@ mod tests { }; assert_eq!(history_command, expected_args); } + + #[test] + fn test_list_logs_events_flag_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "logs", "--events"]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_list_logs_default_values() { + let expected_args = LidarrListCommand::Logs { + events: 500, + output_in_log_format: false, + }; + let result = Cli::try_parse_from(["managarr", "lidarr", "list", "logs"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(logs_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(logs_command, expected_args); + } } mod handler { @@ -136,8 +164,11 @@ mod tests { #[case(LidarrListCommand::Indexers, LidarrEvent::GetIndexers)] #[case(LidarrListCommand::MetadataProfiles, LidarrEvent::GetMetadataProfiles)] #[case(LidarrListCommand::QualityProfiles, LidarrEvent::GetQualityProfiles)] + #[case(LidarrListCommand::QueuedEvents, LidarrEvent::GetQueuedEvents)] #[case(LidarrListCommand::RootFolders, LidarrEvent::GetRootFolders)] #[case(LidarrListCommand::Tags, LidarrEvent::GetTags)] + #[case(LidarrListCommand::Tasks, LidarrEvent::GetTasks)] + #[case(LidarrListCommand::Updates, LidarrEvent::GetUpdates)] #[tokio::test] async fn test_handle_list_command( #[case] list_command: LidarrListCommand, @@ -235,5 +266,33 @@ mod tests { assert_ok!(&result); } + + #[tokio::test] + async fn test_handle_list_logs_command() { + let expected_events = 1000; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetLogs(expected_events).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_logs_command = LidarrListCommand::Logs { + events: 1000, + output_in_log_format: false, + }; + + let result = LidarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } } } diff --git a/src/cli/lidarr/mod.rs b/src/cli/lidarr/mod.rs index 9116820..27a2e38 100644 --- a/src/cli/lidarr/mod.rs +++ b/src/cli/lidarr/mod.rs @@ -14,11 +14,11 @@ use trigger_automatic_search_command_handler::{ LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler, }; +use super::{CliCommandHandler, Command}; +use crate::models::lidarr_models::LidarrTaskName; use crate::network::lidarr_network::LidarrEvent; use crate::{app::App, network::NetworkTrait}; -use super::{CliCommandHandler, Command}; - mod add_command_handler; mod delete_command_handler; mod edit_command_handler; @@ -86,6 +86,16 @@ pub enum LidarrCommand { )] query: String, }, + #[command(about = "Start the specified Lidarr task")] + StartTask { + #[arg( + long, + help = "The name of the task to trigger", + value_enum, + required = true + )] + task_name: LidarrTaskName, + }, #[command( about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'" )] @@ -199,6 +209,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, ' .await?; serde_json::to_string_pretty(&resp)? } + LidarrCommand::StartTask { task_name } => { + let resp = self + .network + .handle_network_event(LidarrEvent::StartTask(task_name).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrCommand::TestIndexer { indexer_id } => { let resp = self .network diff --git a/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs index ff6ef88..1cbc471 100644 --- a/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs @@ -101,9 +101,9 @@ mod tests { assert_eq!( app.data.lidarr_data.main_tabs.get_active_route(), - ActiveLidarrBlock::Artists.into() + ActiveLidarrBlock::System.into() ); - assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + assert_navigation_pushed!(app, ActiveLidarrBlock::System.into()); } #[rstest] diff --git a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs index 83b555f..308d696 100644 --- a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs +++ b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs @@ -52,11 +52,12 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Downloads)] + #[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)] #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)] #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)] #[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)] - #[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Artists)] + #[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)] + #[case(5, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)] fn test_lidarr_handler_change_tab_left_right_keys( #[case] index: usize, #[case] left_block: ActiveLidarrBlock, @@ -85,11 +86,12 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Downloads)] + #[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)] #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)] #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)] #[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)] - #[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Artists)] + #[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)] + #[case(5, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)] fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation( #[case] index: usize, #[case] left_block: ActiveLidarrBlock, @@ -123,6 +125,7 @@ mod tests { #[case(2, ActiveLidarrBlock::History)] #[case(3, ActiveLidarrBlock::RootFolders)] #[case(4, ActiveLidarrBlock::Indexers)] + #[case(5, ActiveLidarrBlock::System)] fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key( #[case] index: usize, #[case] block: ActiveLidarrBlock, @@ -250,4 +253,23 @@ mod tests { active_sonarr_block ); } + + #[rstest] + fn test_delegates_system_blocks_to_system_handler( + #[values( + ActiveLidarrBlock::System, + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates + )] + active_sonarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::System, + active_sonarr_block + ); + } } diff --git a/src/handlers/lidarr_handlers/mod.rs b/src/handlers/lidarr_handlers/mod.rs index 1735b32..e0f6474 100644 --- a/src/handlers/lidarr_handlers/mod.rs +++ b/src/handlers/lidarr_handlers/mod.rs @@ -5,6 +5,7 @@ use library::LibraryHandler; use super::KeyEventHandler; use crate::handlers::lidarr_handlers::downloads::DownloadsHandler; use crate::handlers::lidarr_handlers::root_folders::RootFoldersHandler; +use crate::handlers::lidarr_handlers::system::SystemHandler; use crate::models::Route; use crate::{ app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, @@ -14,11 +15,12 @@ mod downloads; mod history; mod indexers; mod library; +mod root_folders; +mod system; #[cfg(test)] #[path = "lidarr_handler_tests.rs"] mod lidarr_handler_tests; -mod root_folders; pub(super) struct LidarrHandler<'a, 'b> { key: Key, @@ -46,6 +48,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b _ if IndexersHandler::accepts(self.active_lidarr_block) => { IndexersHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); } + _ if SystemHandler::accepts(self.active_lidarr_block) => { + SystemHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } _ => self.handle_key_event(), } } @@ -54,10 +59,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b true } - fn ignore_special_keys(&self) -> bool { - self.app.ignore_special_keys_for_textbox_input - } - fn new( key: Key, app: &'a mut App<'b>, @@ -76,6 +77,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b self.key } + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + fn is_ready(&self) -> bool { true } diff --git a/src/handlers/lidarr_handlers/system/mod.rs b/src/handlers/lidarr_handlers/system/mod.rs new file mode 100644 index 0000000..0faead2 --- /dev/null +++ b/src/handlers/lidarr_handlers/system/mod.rs @@ -0,0 +1,135 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::lidarr_handlers::system::system_details_handler::SystemDetailsHandler; +use crate::handlers::{KeyEventHandler, handle_clear_errors}; +use crate::matches_key; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::models::{Route, Scrollable}; + +mod system_details_handler; + +#[cfg(test)] +#[path = "system_handler_tests.rs"] +mod system_handler_tests; + +pub(super) struct SystemHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for SystemHandler<'a, 'b> { + fn handle(&mut self) { + match self.active_lidarr_block { + _ if SystemDetailsHandler::accepts(self.active_lidarr_block) => { + SystemDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle() + } + _ => self.handle_key_event(), + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + SystemDetailsHandler::accepts(active_block) || active_block == ActiveLidarrBlock::System + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> SystemHandler<'a, 'b> { + SystemHandler { + key, + app, + active_lidarr_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 { + !self.app.is_loading + && !self.app.data.lidarr_data.logs.is_empty() + && !self.app.data.lidarr_data.queued_events.is_empty() + && !self.app.data.lidarr_data.tasks.is_empty() + } + + 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) { + if self.active_lidarr_block == ActiveLidarrBlock::System { + handle_change_tab_left_right_keys(self.app, self.key); + } + } + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) { + handle_clear_errors(self.app) + } + + fn handle_char_key_event(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::System { + let key = self.key; + match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(events, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemQueuedEvents.into()); + } + _ if matches_key!(logs, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemLogs.into()); + self + .app + .data + .lidarr_data + .log_details + .set_items(self.app.data.lidarr_data.logs.items.to_vec()); + self.app.data.lidarr_data.log_details.scroll_to_bottom(); + } + _ if matches_key!(tasks, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + } + _ if matches_key!(update, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemUpdates.into()); + } + _ => (), + } + } + } + + 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/lidarr_handlers/system/system_details_handler.rs b/src/handlers/lidarr_handlers/system/system_details_handler.rs new file mode 100644 index 0000000..01a7b0e --- /dev/null +++ b/src/handlers/lidarr_handlers/system/system_details_handler.rs @@ -0,0 +1,207 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::lidarr_models::LidarrTaskName; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS}; +use crate::models::stateful_list::StatefulList; +use crate::models::{Route, Scrollable}; +use crate::network::lidarr_network::LidarrEvent; + +#[cfg(test)] +#[path = "system_details_handler_tests.rs"] +mod system_details_handler_tests; + +pub(super) struct SystemDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl SystemDetailsHandler<'_, '_> { + fn extract_task_name(&self) -> LidarrTaskName { + self + .app + .data + .lidarr_data + .tasks + .current_selection() + .task_name + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for SystemDetailsHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + SYSTEM_DETAILS_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> SystemDetailsHandler<'a, 'b> { + SystemDetailsHandler { + key, + app, + active_lidarr_block: active_block, + _context: 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 { + !self.app.is_loading + && (!self.app.data.lidarr_data.log_details.is_empty() + || !self.app.data.lidarr_data.tasks.is_empty() + || !self.app.data.lidarr_data.updates.is_empty()) + } + + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_up(), + ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_up(), + ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_up(), + ActiveLidarrBlock::SystemQueuedEvents => self.app.data.lidarr_data.queued_events.scroll_up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_down(), + ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_down(), + ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_down(), + ActiveLidarrBlock::SystemQueuedEvents => { + self.app.data.lidarr_data.queued_events.scroll_down() + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_to_top(), + ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_to_top(), + ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_to_top(), + ActiveLidarrBlock::SystemQueuedEvents => { + self.app.data.lidarr_data.queued_events.scroll_to_top() + } + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_to_bottom(), + ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_to_bottom(), + ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_to_bottom(), + ActiveLidarrBlock::SystemQueuedEvents => { + self.app.data.lidarr_data.queued_events.scroll_to_bottom() + } + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + let key = self.key; + + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => match self.key { + _ if matches_key!(left, key) => { + self + .app + .data + .lidarr_data + .log_details + .items + .iter() + .for_each(|log| log.scroll_right()); + } + _ if matches_key!(right, key) => { + self + .app + .data + .lidarr_data + .log_details + .items + .iter() + .for_each(|log| log.scroll_left()); + } + _ => (), + }, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemTasks => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + } + ActiveLidarrBlock::SystemTaskStartConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::StartTask(self.extract_task_name())); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => { + self.app.data.lidarr_data.log_details = StatefulList::default(); + self.app.pop_navigation_stack() + } + ActiveLidarrBlock::SystemQueuedEvents + | ActiveLidarrBlock::SystemTasks + | ActiveLidarrBlock::SystemUpdates => self.app.pop_navigation_stack(), + ActiveLidarrBlock::SystemTaskStartConfirmPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + if SYSTEM_DETAILS_BLOCKS.contains(&self.active_lidarr_block) && matches_key!(refresh, self.key) + { + self.app.should_refresh = true; + } + + if self.active_lidarr_block == ActiveLidarrBlock::SystemTaskStartConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::StartTask(self.extract_task_name())); + self.app.pop_navigation_stack(); + } + } + + 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/lidarr_handlers/system/system_details_handler_tests.rs b/src/handlers/lidarr_handlers/system/system_details_handler_tests.rs new file mode 100644 index 0000000..0487ef5 --- /dev/null +++ b/src/handlers/lidarr_handlers/system/system_details_handler_tests.rs @@ -0,0 +1,1080 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::system::system_details_handler::SystemDetailsHandler; + use crate::models::lidarr_models::{LidarrTask, LidarrTaskName}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::models::servarr_models::QueueEvent; + use crate::models::{HorizontallyScrollableText, ScrollableText}; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::{HorizontallyScrollableText, ScrollableText}; + use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + + use super::*; + + test_iterable_scroll!( + test_log_details_scroll, + SystemDetailsHandler, + lidarr_data, + log_details, + simple_stateful_iterable_vec!(HorizontallyScrollableText, String, text), + ActiveLidarrBlock::SystemLogs, + None, + text + ); + + #[rstest] + fn test_log_details_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app + .data + .lidarr_data + .log_details + .set_items(simple_stateful_iterable_vec!( + HorizontallyScrollableText, + String, + text + )); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemLogs, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "Test 1" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemLogs, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "Test 1" + ); + } + + #[rstest] + fn test_tasks_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(simple_stateful_iterable_vec!(LidarrTask, String, name)); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 2" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_tasks_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(simple_stateful_iterable_vec!(LidarrTask, String, name)); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_queued_events_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .lidarr_data + .queued_events + .set_items(simple_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 2" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_queued_events_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .lidarr_data + .queued_events + .set_items(simple_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_system_updates_scroll() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 1); + } + + #[test] + fn test_system_updates_scroll_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + } + } + + mod test_handle_home_end { + use crate::models::{HorizontallyScrollableText, ScrollableText}; + use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + + use super::*; + use pretty_assertions::assert_eq; + + test_iterable_home_and_end!( + test_log_details_home_end, + SystemDetailsHandler, + lidarr_data, + log_details, + extended_stateful_iterable_vec!(HorizontallyScrollableText, String, text), + ActiveLidarrBlock::SystemLogs, + None, + text + ); + + #[test] + fn test_log_details_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app + .data + .lidarr_data + .log_details + .set_items(extended_stateful_iterable_vec!( + HorizontallyScrollableText, + String, + text + )); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemLogs, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "Test 1" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemLogs, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "Test 1" + ); + } + + #[test] + fn test_tasks_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(extended_stateful_iterable_vec!(LidarrTask, String, name)); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 3" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_tasks_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(extended_stateful_iterable_vec!(LidarrTask, String, name)); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_queued_events_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .lidarr_data + .queued_events + .set_items(extended_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 3" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_queued_events_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .lidarr_data + .queued_events + .set_items(extended_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_system_updates_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 1); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + } + + #[test] + fn test_system_updates_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[test] + fn test_handle_log_details_left_right() { + let active_lidarr_block = ActiveLidarrBlock::SystemLogs; + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app + .data + .lidarr_data + .log_details + .set_items(vec!["t1".into(), "t22".into()]); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "t22"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "22"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "2"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), ""); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), ""); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "2"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "22"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "t22"); + } + + #[rstest] + fn test_left_right_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + SystemDetailsHandler::new( + key, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + SystemDetailsHandler::new( + key, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::assert_navigation_popped; + use crate::network::lidarr_network::LidarrEvent; + use pretty_assertions::assert_eq; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_system_tasks_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + SystemDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::SystemTasks, None) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + } + + #[test] + fn test_system_tasks_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + SystemDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::SystemTasks, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::SystemTasks.into() + ); + } + + #[test] + fn test_system_tasks_start_task_prompt_confirm_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.data.lidarr_data.prompt_confirm = true; + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::StartTask(LidarrTaskName::default()) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::SystemTasks.into()); + } + + #[test] + fn test_system_tasks_start_task_prompt_decline_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveLidarrBlock::SystemTasks.into()); + } + } + + mod test_handle_esc { + use crate::models::HorizontallyScrollableText; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_esc_system_logs(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app + .data + .lidarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::from("test")]); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemLogs.into()); + app + .data + .lidarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); + + SystemDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::SystemLogs, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + assert_is_empty!(app.data.lidarr_data.log_details.items); + } + + #[rstest] + fn test_esc_system_tasks(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + } + + #[rstest] + fn test_esc_system_queued_events(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemQueuedEvents.into()); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + SystemDetailsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + } + + #[rstest] + fn test_esc_system_updates(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemUpdates.into()); + + SystemDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::SystemUpdates, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + } + + #[test] + fn test_system_tasks_start_task_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + SystemDetailsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::SystemTasks.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::{assert_navigation_popped, assert_navigation_pushed}; + + #[rstest] + fn test_refresh_key( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(active_lidarr_block.into()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!(app, active_lidarr_block.into()); + assert!(app.should_refresh); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(active_lidarr_block.into()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_system_tasks_start_task_prompt_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::StartTask(LidarrTaskName::default())) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::SystemTasks.into()); + } + } + + #[test] + fn test_system_details_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if SYSTEM_DETAILS_BLOCKS.contains(&active_lidarr_block) { + assert!(SystemDetailsHandler::accepts(active_lidarr_block)); + } else { + assert!(!SystemDetailsHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_system_details_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 = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_task_name() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + let task_name = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .extract_task_name(); + + assert_eq!(task_name, LidarrTaskName::default()); + } + + #[test] + fn test_system_details_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_system_details_handler_not_ready_when_log_details_and_updates_and_tasks_are_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_log_details_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app + .data + .lidarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_tasks_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_updates_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/system/system_handler_tests.rs b/src/handlers/lidarr_handlers/system/system_handler_tests.rs new file mode 100644 index 0000000..ef8271c --- /dev/null +++ b/src/handlers/lidarr_handlers/system/system_handler_tests.rs @@ -0,0 +1,580 @@ +#[cfg(test)] +mod tests { + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::system::SystemHandler; + use crate::models::lidarr_models::LidarrTask; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::models::servarr_models::QueueEvent; + use crate::test_handler_delegation; + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_system_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(5); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Indexers.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into()); + } + + #[rstest] + fn test_system_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(5); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Artists.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + } + } + + mod test_handle_esc { + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_default_esc(#[values(true, false)] is_loading: bool) { + let mut app = App::test_default(); + app.is_loading = is_loading; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + SystemHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::System, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::models::HorizontallyScrollableText; + + use super::*; + + #[test] + fn test_update_system_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemUpdates.into()); + } + + #[test] + fn test_update_system_key_no_op_if_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + } + + #[test] + fn test_queued_events_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.events.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemQueuedEvents.into()); + } + + #[test] + fn test_queued_events_key_no_op_if_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.events.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + } + + #[test] + fn test_refresh_system_key() { + let mut app = App::test_default(); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::System.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_system_key_no_op_if_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_logs_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.logs.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemLogs.into()); + assert_eq!( + app.data.lidarr_data.log_details.items, + app.data.lidarr_data.logs.items + ); + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "test 2" + ); + } + + #[test] + fn test_logs_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.logs.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + assert_is_empty!(app.data.lidarr_data.log_details); + } + + #[test] + fn test_tasks_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.tasks.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemTasks.into()); + } + + #[test] + fn test_tasks_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.tasks.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + } + } + + #[rstest] + fn test_delegates_system_details_blocks_to_system_details_handler( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + SystemHandler, + ActiveLidarrBlock::System, + active_lidarr_block + ); + } + + #[test] + fn test_system_handler_accepts() { + let mut system_blocks = vec![ActiveLidarrBlock::System]; + system_blocks.extend(SYSTEM_DETAILS_BLOCKS); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if system_blocks.contains(&active_lidarr_block) { + assert!(SystemHandler::accepts(active_lidarr_block)); + } else { + assert!(!SystemHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_system_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 = SystemHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_system_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_logs_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_tasks_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app.data.lidarr_data.logs.set_items(vec!["test".into()]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_queued_events_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app.data.lidarr_data.logs.set_items(vec!["test".into()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_ready_when_all_required_tables_are_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app.data.lidarr_data.logs.set_items(vec!["test".into()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(system_handler.is_ready()); + } +} diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index db612eb..15fec55 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -5,13 +5,15 @@ use super::{ SecurityConfig, Tag, }, }; -use crate::models::servarr_models::IndexerSettings; +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)] @@ -426,6 +428,42 @@ pub struct LidarrHistoryItem { pub data: LidarrHistoryData, } +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LidarrTask { + pub name: String, + pub task_name: LidarrTaskName, + #[serde(deserialize_with = "super::from_i64")] + pub interval: i64, + pub last_execution: DateTime, + pub next_execution: DateTime, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)] +#[serde(rename_all = "PascalCase")] +pub enum LidarrTaskName { + #[default] + ApplicationUpdateCheck, + Backup, + CheckHealth, + Housekeeping, + ImportListSync, + MessagingCleanup, + RefreshArtist, + RefreshMonitoredDownloads, + RescanFolders, + RssSync, +} + +impl Display for LidarrTaskName { + 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}") + } +} + impl From for Serdeable { fn from(value: LidarrSerdeable) -> Serdeable { Serdeable::Lidarr(value) @@ -446,13 +484,17 @@ serde_enum_from!( IndexerSettings(IndexerSettings), Indexers(Vec), IndexerTestResults(Vec), + LogResponse(LogResponse), MetadataProfiles(Vec), QualityProfiles(Vec), + QueueEvents(Vec), RootFolders(Vec), SecurityConfig(SecurityConfig), SystemStatus(SystemStatus), Tag(Tag), Tags(Vec), + Tasks(Vec), + Updates(Vec), Value(Value), } ); diff --git a/src/models/lidarr_models_tests.rs b/src/models/lidarr_models_tests.rs index 9053e59..00145d9 100644 --- a/src/models/lidarr_models_tests.rs +++ b/src/models/lidarr_models_tests.rs @@ -6,12 +6,12 @@ mod tests { use crate::models::lidarr_models::{ AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse, - LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, Member, MetadataProfile, - MonitorType, NewItemMonitorType, SystemStatus, + LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, Member, + MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus, }; use crate::models::servarr_models::{ - DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, QualityProfile, RootFolder, - SecurityConfig, Tag, + DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse, + QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update, }; use crate::models::{ Serdeable, @@ -363,6 +363,20 @@ mod tests { ); } + #[test] + fn test_lidarr_serdeable_from_log_response() { + let log_response = LogResponse { + records: vec![Log { + level: "info".to_owned(), + ..Log::default() + }], + }; + + let lidarr_serdeable: LidarrSerdeable = log_response.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::LogResponse(log_response)); + } + #[test] fn test_lidarr_serdeable_from_metadata_profiles() { let metadata_profiles = vec![MetadataProfile { @@ -405,6 +419,18 @@ mod tests { ); } + #[test] + fn test_lidarr_serdeable_from_queue_events() { + let queue_events = vec![QueueEvent { + trigger: "test".to_owned(), + ..QueueEvent::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = queue_events.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::QueueEvents(queue_events)); + } + #[test] fn test_lidarr_serdeable_from_root_folders() { let root_folders = vec![RootFolder { @@ -501,6 +527,30 @@ mod tests { assert_eq!(lidarr_serdeable, LidarrSerdeable::Album(album)); } + #[test] + fn test_lidarr_serdeable_from_tasks() { + let tasks = vec![LidarrTask { + name: "test".to_owned(), + ..LidarrTask::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = tasks.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Tasks(tasks)); + } + + #[test] + fn test_lidarr_serdeable_from_updates() { + let updates = vec![Update { + version: "test".to_owned(), + ..Update::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = updates.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Updates(updates)); + } + #[test] fn test_artist_status_display() { assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing"); diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs index 0671396..7768255 100644 --- a/src/models/servarr_data/lidarr/lidarr_data.rs +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -3,15 +3,17 @@ use serde_json::Number; use super::modals::{AddArtistModal, AddRootFolderModal, EditArtistModal}; use crate::app::context_clues::{ DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - ROOT_FOLDERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::lidarr::lidarr_context_clues::{ ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, }; +use crate::models::lidarr_models::LidarrTask; use crate::models::servarr_data::modals::EditIndexerModal; -use crate::models::servarr_models::IndexerSettings; +use crate::models::servarr_models::{IndexerSettings, QueueEvent}; +use crate::models::stateful_list::StatefulList; use crate::models::{ - BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState, + BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState, lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord, LidarrHistoryItem}, servarr_data::modals::IndexerTestResultModalItem, servarr_models::{DiskSpace, Indexer, RootFolder}, @@ -32,8 +34,11 @@ use { add_artist_search_result, album, artist, download_record, indexer, lidarr_history_item, metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map, }, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{log_line, task}, crate::network::servarr_test_utils::diskspace, crate::network::servarr_test_utils::indexer_test_result, + crate::network::servarr_test_utils::queued_event, + crate::network::sonarr_network::sonarr_network_test_utils::test_utils::updates, strum::{Display, EnumString, IntoEnumIterator}, }; @@ -60,15 +65,20 @@ pub struct LidarrData<'a> { pub indexer_settings: Option, pub indexer_test_all_results: Option>, pub indexer_test_errors: Option, + pub logs: StatefulList, + pub log_details: 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, ActiveLidarrBlock>, pub start_time: DateTime, pub tags_map: BiMap, + pub tasks: StatefulTable, + pub updates: ScrollableText, pub version: String, } @@ -135,14 +145,19 @@ impl<'a> Default for LidarrData<'a> { indexer_settings: None, indexer_test_all_results: None, indexer_test_errors: None, + logs: StatefulList::default(), + log_details: 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 { @@ -175,6 +190,12 @@ impl<'a> Default for LidarrData<'a> { contextual_help: Some(&INDEXERS_CONTEXT_CLUES), config: None, }, + TabRoute { + title: "System".to_string(), + route: ActiveLidarrBlock::System.into(), + contextual_help: Some(&SYSTEM_CONTEXT_CLUES), + config: None, + }, ]), artist_info_tabs: TabState::new(vec![TabRoute { title: "Albums".to_string(), @@ -270,7 +291,10 @@ impl LidarrData<'_> { indexer_settings: Some(indexer_settings()), indexer_test_all_results: Some(indexer_test_all_results), indexer_test_errors: Some("error".to_string()), + start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()), tags_map: tags_map(), + updates: updates(), + version: "1.2.3.4".to_owned(), ..LidarrData::default() }; lidarr_data.albums.set_items(vec![album()]); @@ -292,11 +316,14 @@ impl LidarrData<'_> { lidarr_data.history.filter = Some("test filter".into()); lidarr_data.root_folders.set_items(vec![root_folder()]); lidarr_data.indexers.set_items(vec![indexer()]); - lidarr_data.version = "1.0.0".to_owned(); + lidarr_data.queued_events.set_items(vec![queued_event()]); lidarr_data.add_artist_search = Some("Test Artist".into()); let mut add_searched_artists = StatefulTable::default(); add_searched_artists.set_items(vec![add_artist_search_result()]); lidarr_data.add_searched_artists = Some(add_searched_artists); + lidarr_data.logs.set_items(vec![log_line().into()]); + lidarr_data.log_details.set_items(vec![log_line().into()]); + lidarr_data.tasks.set_items(vec![task()]); lidarr_data } @@ -385,6 +412,12 @@ pub enum ActiveLidarrBlock { SearchArtistsError, SearchHistory, SearchHistoryError, + System, + SystemLogs, + SystemQueuedEvents, + SystemTasks, + SystemTaskStartConfirmPrompt, + SystemUpdates, UpdateAllArtistsPrompt, UpdateAndScanArtistPrompt, UpdateDownloadsPrompt, @@ -611,6 +644,14 @@ pub static INDEXERS_BLOCKS: [ActiveLidarrBlock; 3] = [ ActiveLidarrBlock::TestIndexer, ]; +pub static SYSTEM_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [ + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates, +]; + impl From for Route { fn from(active_lidarr_block: ActiveLidarrBlock) -> Route { Route::Lidarr(active_lidarr_block, None) diff --git a/src/models/servarr_data/lidarr/lidarr_data_tests.rs b/src/models/servarr_data/lidarr/lidarr_data_tests.rs index 8bc7c94..7bafbc9 100644 --- a/src/models/servarr_data/lidarr/lidarr_data_tests.rs +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -2,7 +2,7 @@ mod tests { use crate::app::context_clues::{ DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - ROOT_FOLDERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::lidarr::lidarr_context_clues::{ ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, @@ -14,7 +14,7 @@ mod tests { DELETE_ARTIST_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS, - INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, + INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS, }; use crate::models::{ BlockSelectionState, Route, @@ -143,17 +143,22 @@ mod tests { assert_none!(lidarr_data.edit_artist_modal); assert_none!(lidarr_data.add_root_folder_modal); assert_is_empty!(lidarr_data.history); + assert_is_empty!(lidarr_data.logs); + assert_is_empty!(lidarr_data.log_details); assert_is_empty!(lidarr_data.metadata_profile_map); assert!(!lidarr_data.prompt_confirm); assert_none!(lidarr_data.prompt_confirm_action); assert_is_empty!(lidarr_data.quality_profile_map); + assert_is_empty!(lidarr_data.queued_events); assert_is_empty!(lidarr_data.root_folders); assert_eq!(lidarr_data.selected_block, BlockSelectionState::default()); assert_eq!(lidarr_data.start_time, >::default()); assert_is_empty!(lidarr_data.tags_map); + assert_is_empty!(lidarr_data.tasks); + assert_is_empty!(lidarr_data.updates); assert_is_empty!(lidarr_data.version); - assert_eq!(lidarr_data.main_tabs.tabs.len(), 5); + assert_eq!(lidarr_data.main_tabs.tabs.len(), 6); assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -210,6 +215,17 @@ mod tests { ); assert_none!(lidarr_data.main_tabs.tabs[4].config); + assert_str_eq!(lidarr_data.main_tabs.tabs[5].title, "System"); + assert_eq!( + lidarr_data.main_tabs.tabs[5].route, + ActiveLidarrBlock::System.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[5].contextual_help, + &SYSTEM_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[5].config); + assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1); assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums"); assert_eq!( @@ -605,4 +621,14 @@ mod tests { ); assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderTagsInput)); } + + #[test] + fn test_system_details_blocks_contents() { + assert_eq!(SYSTEM_DETAILS_BLOCKS.len(), 5); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemLogs)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemQueuedEvents)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTasks)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTaskStartConfirmPrompt)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemUpdates)); + } } diff --git a/src/network/lidarr_network/lidarr_network_test_utils.rs b/src/network/lidarr_network/lidarr_network_test_utils.rs index 8963315..2024434 100644 --- a/src/network/lidarr_network/lidarr_network_test_utils.rs +++ b/src/network/lidarr_network/lidarr_network_test_utils.rs @@ -1,19 +1,20 @@ #[cfg(test)] #[allow(dead_code)] pub mod test_utils { - use crate::models::HorizontallyScrollableText; use crate::models::lidarr_models::{ AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus, DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, LidarrHistoryData, - LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, Member, MetadataProfile, - NewItemMonitorType, Ratings, SystemStatus, + LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, LidarrTaskName, + Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus, }; use crate::models::servarr_models::IndexerSettings; use crate::models::servarr_models::{ Indexer, IndexerField, Quality, QualityProfile, QualityWrapper, RootFolder, Tag, }; + use crate::models::{HorizontallyScrollableText, ScrollableText}; use bimap::BiMap; use chrono::DateTime; + use indoc::formatdoc; use serde_json::{Number, json}; pub const ADD_ARTIST_SEARCH_RESULT_JSON: &str = r#"{ @@ -333,4 +334,47 @@ pub mod test_utils { rss_sync_interval: 60, } } + + pub fn task() -> LidarrTask { + LidarrTask { + name: "Backup".to_owned(), + task_name: LidarrTaskName::Backup, + interval: 60, + last_execution: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()), + next_execution: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T22:29:16Z").unwrap()), + } + } + + pub fn log_line() -> &'static str { + "2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process" + } + + pub fn updates() -> ScrollableText { + let line_break = "-".repeat(200); + ScrollableText::with_string(formatdoc!( + " + The latest version of Lidarr is already installed + + 4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed) + {line_break} + New: + * Cool new thing + Fixed: + * Some bugs killed + + + 3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed) + {line_break} + New: + * Cool new thing (old) + * Other cool new thing (old) + + + 2.1.0 - 2023-04-15 02:02:53 UTC + {line_break} + Fixed: + * Killed bug 1 + * Fixed bug 2" + )) + } } diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index 380c533..c6b0e19 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -90,7 +90,9 @@ mod tests { LidarrEvent::UpdateAllArtists, LidarrEvent::TriggerAutomaticArtistSearch(0), LidarrEvent::UpdateAndScanArtist(0), - LidarrEvent::UpdateDownloads + LidarrEvent::UpdateDownloads, + LidarrEvent::GetQueuedEvents, + LidarrEvent::StartTask(Default::default()) )] event: LidarrEvent, ) { @@ -128,6 +130,9 @@ mod tests { #[case(LidarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(LidarrEvent::GetStatus, "/system/status")] #[case(LidarrEvent::GetTags, "/tag")] + #[case(LidarrEvent::GetLogs(500), "/log")] + #[case(LidarrEvent::GetTasks, "/system/task")] + #[case(LidarrEvent::GetUpdates, "/update")] #[case(LidarrEvent::HealthCheck, "/health")] #[case(LidarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")] #[case(LidarrEvent::TestIndexer(0), "/indexer/test")] diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index 148f2a9..dd82616 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -4,7 +4,7 @@ use log::info; use super::{NetworkEvent, NetworkResource}; use crate::models::lidarr_models::{ AddArtistBody, AddLidarrRootFolderBody, DeleteParams, EditArtistParams, LidarrSerdeable, - MetadataProfile, + LidarrTaskName, MetadataProfile, }; use crate::models::servarr_models::{EditIndexerParams, IndexerSettings, QualityProfile, Tag}; use crate::network::{Network, RequestMethod}; @@ -47,16 +47,21 @@ pub enum LidarrEvent { GetHistory(u64), GetHostConfig, GetIndexers, + GetLogs(u64), MarkHistoryItemAsFailed(i64), GetMetadataProfiles, GetQualityProfiles, + GetQueuedEvents, GetRootFolders, GetSecurityConfig, GetStatus, + GetUpdates, GetTags, + GetTasks, HealthCheck, ListArtists, SearchNewArtist(String), + StartTask(LidarrTaskName), TestIndexer(i64), TestAllIndexers, ToggleAlbumMonitoring(i64), @@ -84,6 +89,7 @@ impl NetworkResource for LidarrEvent { | LidarrEvent::ToggleAlbumMonitoring(_) | LidarrEvent::GetAlbumDetails(_) | LidarrEvent::DeleteAlbum(_) => "/album", + LidarrEvent::GetLogs(_) => "/log", LidarrEvent::GetDiskSpace => "/diskspace", LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue", LidarrEvent::GetHistory(_) => "/history", @@ -95,7 +101,9 @@ impl NetworkResource for LidarrEvent { LidarrEvent::TriggerAutomaticArtistSearch(_) | LidarrEvent::UpdateAllArtists | LidarrEvent::UpdateAndScanArtist(_) - | LidarrEvent::UpdateDownloads => "/command", + | LidarrEvent::UpdateDownloads + | LidarrEvent::GetQueuedEvents + | LidarrEvent::StartTask(_) => "/command", LidarrEvent::GetMetadataProfiles => "/metadataprofile", LidarrEvent::GetQualityProfiles => "/qualityprofile", LidarrEvent::GetRootFolders @@ -104,6 +112,8 @@ impl NetworkResource for LidarrEvent { LidarrEvent::TestIndexer(_) => "/indexer/test", LidarrEvent::TestAllIndexers => "/indexer/testall", LidarrEvent::GetStatus => "/system/status", + LidarrEvent::GetTasks => "/system/task", + LidarrEvent::GetUpdates => "/update", LidarrEvent::HealthCheck => "/health", LidarrEvent::SearchNewArtist(_) => "/artist/lookup", } @@ -182,6 +192,10 @@ impl Network<'_, '_> { .get_lidarr_history(events) .await .map(LidarrSerdeable::from), + LidarrEvent::GetLogs(events) => self + .get_lidarr_logs(events) + .await + .map(LidarrSerdeable::from), LidarrEvent::MarkHistoryItemAsFailed(history_item_id) => self .mark_lidarr_history_item_as_failed(history_item_id) .await @@ -198,6 +212,10 @@ impl Network<'_, '_> { .get_lidarr_quality_profiles() .await .map(LidarrSerdeable::from), + LidarrEvent::GetQueuedEvents => self + .get_queued_lidarr_events() + .await + .map(LidarrSerdeable::from), LidarrEvent::GetRootFolders => self .get_lidarr_root_folders() .await @@ -208,6 +226,8 @@ impl Network<'_, '_> { .map(LidarrSerdeable::from), LidarrEvent::GetStatus => self.get_lidarr_status().await.map(LidarrSerdeable::from), LidarrEvent::GetTags => self.get_lidarr_tags().await.map(LidarrSerdeable::from), + LidarrEvent::GetTasks => self.get_lidarr_tasks().await.map(LidarrSerdeable::from), + LidarrEvent::GetUpdates => self.get_lidarr_updates().await.map(LidarrSerdeable::from), LidarrEvent::HealthCheck => self .get_lidarr_healthcheck() .await @@ -216,6 +236,10 @@ impl Network<'_, '_> { LidarrEvent::SearchNewArtist(query) => { self.search_artist(query).await.map(LidarrSerdeable::from) } + LidarrEvent::StartTask(task_name) => self + .start_lidarr_task(task_name) + .await + .map(LidarrSerdeable::from), LidarrEvent::ToggleAlbumMonitoring(album_id) => self .toggle_album_monitoring(album_id) .await diff --git a/src/network/lidarr_network/system/lidarr_system_network_tests.rs b/src/network/lidarr_network/system/lidarr_system_network_tests.rs index 2c9f52c..ab97ab8 100644 --- a/src/network/lidarr_network/system/lidarr_system_network_tests.rs +++ b/src/network/lidarr_network/system/lidarr_system_network_tests.rs @@ -1,9 +1,14 @@ #[cfg(test)] mod tests { - use crate::models::lidarr_models::{LidarrSerdeable, SystemStatus}; - use crate::models::servarr_models::{DiskSpace, HostConfig, SecurityConfig}; + use crate::models::HorizontallyScrollableText; + use crate::models::lidarr_models::{LidarrSerdeable, LidarrTask, LidarrTaskName, SystemStatus}; + use crate::models::servarr_models::{ + DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update, + }; use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::updates; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use chrono::DateTime; use pretty_assertions::assert_eq; use serde_json::json; @@ -104,6 +109,117 @@ mod tests { assert_eq!(security_config, response); } + #[tokio::test] + async fn test_handle_get_lidarr_logs_event() { + let expected_logs = vec![ + HorizontallyScrollableText::from( + "2023-05-20 21:29:16 UTC|FATAL|LidarrError|Some.Big.Bad.Exception|test exception", + ), + HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), + ]; + let logs_response_json = json!({ + "page": 1, + "pageSize": 500, + "sortKey": "time", + "sortDirection": "descending", + "totalRecords": 2, + "records": [ + { + "time": "2023-05-20T21:29:16Z", + "level": "info", + "logger": "TestLogger", + "message": "test message", + "id": 1 + }, + { + "time": "2023-05-20T21:29:16Z", + "level": "fatal", + "logger": "LidarrError", + "exception": "test exception", + "exceptionType": "Some.Big.Bad.Exception", + "id": 2 + } + ] + }); + let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(logs_response_json) + .query("pageSize=500&sortDirection=descending&sortKey=time") + .build_for(LidarrEvent::GetLogs(500)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LogResponse(logs) = network + .handle_lidarr_event(LidarrEvent::GetLogs(500)) + .await + .unwrap() + else { + panic!("Expected LogResponse") + }; + mock.assert_async().await; + assert_eq!(app.lock().await.data.lidarr_data.logs.items, expected_logs); + assert!( + app + .lock() + .await + .data + .lidarr_data + .logs + .current_selection() + .text + .contains("INFO") + ); + assert_eq!(logs, response); + } + + #[tokio::test] + async fn test_handle_get_queued_lidarr_events_event() { + let queued_events_json = json!([{ + "name": "RefreshMonitoredDownloads", + "commandName": "Refresh Monitored Downloads", + "status": "completed", + "queued": "2023-05-20T21:29:16Z", + "started": "2023-05-20T21:29:16Z", + "ended": "2023-05-20T21:29:16Z", + "duration": "00:00:00.5111547", + "trigger": "scheduled", + }]); + let response: Vec = serde_json::from_value(queued_events_json.clone()).unwrap(); + let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); + let expected_event = QueueEvent { + name: "RefreshMonitoredDownloads".to_owned(), + command_name: "Refresh Monitored Downloads".to_owned(), + status: "completed".to_owned(), + queued: timestamp, + started: Some(timestamp), + ended: Some(timestamp), + duration: Some("00:00:00.5111547".to_owned()), + trigger: "scheduled".to_owned(), + }; + + let (mock, app, _server) = MockServarrApi::get() + .returns(queued_events_json) + .build_for(LidarrEvent::GetQueuedEvents) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::QueueEvents(events) = network + .handle_lidarr_event(LidarrEvent::GetQueuedEvents) + .await + .unwrap() + else { + panic!("Expected QueueEvents") + }; + mock.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.queued_events.items, + vec![expected_event] + ); + assert_eq!(events, response); + } + #[tokio::test] async fn test_handle_get_status_event() { let status_json = json!({ @@ -129,4 +245,171 @@ mod tests { assert_eq!(status, response); assert_eq!(app.lock().await.data.lidarr_data.version, "1.0.0"); } + + #[tokio::test] + async fn test_handle_get_lidarr_tasks_event() { + let tasks_json = json!([{ + "name": "Application Update Check", + "taskName": "ApplicationUpdateCheck", + "interval": 360, + "lastExecution": "2023-05-20T21:29:16Z", + "nextExecution": "2023-05-20T21:29:16Z", + }, + { + "name": "Backup", + "taskName": "Backup", + "interval": 10080, + "lastExecution": "2023-05-20T21:29:16Z", + "nextExecution": "2023-05-20T21:29:16Z", + }]); + let response: Vec = serde_json::from_value(tasks_json.clone()).unwrap(); + let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); + let expected_tasks = vec![ + LidarrTask { + name: "Application Update Check".to_owned(), + task_name: LidarrTaskName::ApplicationUpdateCheck, + interval: 360, + last_execution: timestamp, + next_execution: timestamp, + }, + LidarrTask { + name: "Backup".to_owned(), + task_name: LidarrTaskName::Backup, + interval: 10080, + last_execution: timestamp, + next_execution: timestamp, + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(tasks_json) + .build_for(LidarrEvent::GetTasks) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Tasks(tasks) = network + .handle_lidarr_event(LidarrEvent::GetTasks) + .await + .unwrap() + else { + panic!("Expected Tasks") + }; + mock.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.tasks.items, + expected_tasks + ); + assert_eq!(tasks, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_updates_event() { + let updates_json = json!([{ + "version": "4.3.2.1", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": true, + "installedOn": "2023-04-15T02:02:53Z", + "latest": true, + "changes": { + "new": [ + "Cool new thing" + ], + "fixed": [ + "Some bugs killed" + ] + }, + }, + { + "version": "3.2.1.0", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": false, + "installedOn": "2023-04-15T02:02:53Z", + "latest": false, + "changes": { + "new": [ + "Cool new thing (old)", + "Other cool new thing (old)" + ], + }, + }, + { + "version": "2.1.0", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": false, + "latest": false, + "changes": { + "fixed": [ + "Killed bug 1", + "Fixed bug 2" + ] + }, + }]); + let response: Vec = serde_json::from_value(updates_json.clone()).unwrap(); + let expected_text = updates(); + let (mock, app, _server) = MockServarrApi::get() + .returns(updates_json) + .build_for(LidarrEvent::GetUpdates) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Updates(updates) = network + .handle_lidarr_event(LidarrEvent::GetUpdates) + .await + .unwrap() + else { + panic!("Expected Updates") + }; + mock.assert_async().await; + let actual_text = app.lock().await.data.lidarr_data.updates.get_text(); + let expected = expected_text.get_text(); + + // Trim trailing whitespace from each line for comparison + let actual_trimmed: Vec<&str> = actual_text.lines().map(|l| l.trim_end()).collect(); + let expected_trimmed: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect(); + + assert_eq!( + actual_trimmed, expected_trimmed, + "Updates text mismatch (after trimming trailing whitespace)" + ); + assert_eq!(updates, response); + } + + #[tokio::test] + async fn test_handle_start_lidarr_task_event() { + let response = json!({ "test": "test"}); + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "ApplicationUpdateCheck" + })) + .returns(response.clone()) + .build_for(LidarrEvent::StartTask( + LidarrTaskName::ApplicationUpdateCheck, + )) + .await; + app + .lock() + .await + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask { + task_name: LidarrTaskName::default(), + ..LidarrTask::default() + }]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Value(value) = network + .handle_lidarr_event(LidarrEvent::StartTask( + LidarrTaskName::ApplicationUpdateCheck, + )) + .await + .unwrap() + else { + panic!("Expected Value") + }; + mock.assert_async().await; + assert_eq!(value, response); + } } diff --git a/src/network/lidarr_network/system/mod.rs b/src/network/lidarr_network/system/mod.rs index 1b33ac2..fb1b1cf 100644 --- a/src/network/lidarr_network/system/mod.rs +++ b/src/network/lidarr_network/system/mod.rs @@ -1,10 +1,14 @@ -use anyhow::Result; -use log::info; - -use crate::models::lidarr_models::SystemStatus; -use crate::models::servarr_models::{DiskSpace, HostConfig, SecurityConfig}; +use crate::models::lidarr_models::{LidarrTask, LidarrTaskName, SystemStatus}; +use crate::models::servarr_models::{ + CommandBody, DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update, +}; +use crate::models::{HorizontallyScrollableText, Scrollable, ScrollableText}; use crate::network::lidarr_network::LidarrEvent; use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use indoc::formatdoc; +use log::info; +use serde_json::Value; #[cfg(test)] #[path = "lidarr_system_network_tests.rs"] @@ -26,18 +30,62 @@ impl Network<'_, '_> { .await } - pub(in crate::network::lidarr_network) async fn get_lidarr_security_config( + pub(in crate::network::lidarr_network) async fn get_lidarr_logs( &mut self, - ) -> Result { - info!("Fetching Lidarr security config"); - let event = LidarrEvent::GetSecurityConfig; + events: u64, + ) -> Result { + info!("Fetching Lidarr logs"); + let event = LidarrEvent::GetLogs(events); + let params = format!("pageSize={events}&sortDirection=descending&sortKey=time"); let request_props = self - .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) .await; self - .handle_request::<(), SecurityConfig>(request_props, |_, _| ()) + .handle_request::<(), LogResponse>(request_props, |log_response, mut app| { + let mut logs = log_response.records; + logs.reverse(); + + let log_lines = logs + .into_iter() + .map(|log| { + if log.exception.is_some() { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log + .logger + .as_ref() + .expect("logger must exist when exception is present"), + log + .exception_type + .as_ref() + .expect("exception_type must exist when exception is present"), + log + .exception + .as_ref() + .expect("exception must exist in this branch") + )) + } else { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log.logger.as_ref().expect("logger must exist in log entry"), + log + .message + .as_ref() + .expect("message must exist when exception is not present") + )) + } + }) + .collect(); + + app.data.lidarr_data.logs.set_items(log_lines); + app.data.lidarr_data.logs.scroll_to_bottom(); + }) .await } @@ -58,6 +106,42 @@ impl Network<'_, '_> { .await } + pub(in crate::network::lidarr_network) async fn get_queued_lidarr_events( + &mut self, + ) -> Result> { + info!("Fetching Lidarr queued events"); + let event = LidarrEvent::GetQueuedEvents; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |queued_events_vec, mut app| { + app + .data + .lidarr_data + .queued_events + .set_items(queued_events_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_security_config( + &mut self, + ) -> Result { + info!("Fetching Lidarr security config"); + let event = LidarrEvent::GetSecurityConfig; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), SecurityConfig>(request_props, |_, _| ()) + .await + } + pub(in crate::network::lidarr_network) async fn get_lidarr_status( &mut self, ) -> Result { @@ -75,4 +159,120 @@ impl Network<'_, '_> { }) .await } + + pub(in crate::network::lidarr_network) async fn get_lidarr_tasks( + &mut self, + ) -> Result> { + info!("Fetching Lidarr tasks"); + let event = LidarrEvent::GetTasks; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |tasks_vec, mut app| { + app.data.lidarr_data.tasks.set_items(tasks_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_updates( + &mut self, + ) -> Result> { + info!("Fetching Lidarr updates"); + let event = LidarrEvent::GetUpdates; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |updates_vec, mut app| { + let latest_installed = if updates_vec + .iter() + .any(|update| update.latest && update.installed_on.is_some()) + { + "already".to_owned() + } else { + "not".to_owned() + }; + let updates = updates_vec + .into_iter() + .map(|update| { + let install_status = if update.installed_on.is_some() { + if update.installed { + " (Currently Installed)".to_owned() + } else { + " (Previously Installed)".to_owned() + } + } else { + String::new() + }; + let vec_to_bullet_points = |vec: Vec| { + vec + .iter() + .map(|change| format!(" * {change}")) + .collect::>() + .join("\n") + }; + + let mut update_info = formatdoc!( + "{} - {}{install_status} + {}", + update.version, + update.release_date, + "-".repeat(200) + ); + + if let Some(new_changes) = update.changes.new { + let changes = vec_to_bullet_points(new_changes); + update_info = formatdoc!( + "{update_info} + New: + {changes}" + ) + } + + if let Some(fixes) = update.changes.fixed { + let fixes = vec_to_bullet_points(fixes); + update_info = formatdoc!( + "{update_info} + Fixed: + {fixes}" + ); + } + + update_info + }) + .reduce(|version_1, version_2| format!("{version_1}\n\n\n{version_2}")) + .unwrap(); + + app.data.lidarr_data.updates = ScrollableText::with_string(formatdoc!( + "The latest version of Lidarr is {latest_installed} installed + + {updates}" + )); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn start_lidarr_task( + &mut self, + task: LidarrTaskName, + ) -> Result { + let event = LidarrEvent::StartTask(task); + let task_name = task.to_string(); + info!("Starting Lidarr task: {task_name}"); + + let body = CommandBody { name: task_name }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } } diff --git a/src/ui/lidarr_ui/mod.rs b/src/ui/lidarr_ui/mod.rs index 85fc534..749dd5a 100644 --- a/src/ui/lidarr_ui/mod.rs +++ b/src/ui/lidarr_ui/mod.rs @@ -26,6 +26,7 @@ use super::{ use crate::ui::lidarr_ui::downloads::DownloadsUi; use crate::ui::lidarr_ui::indexers::IndexersUi; use crate::ui::lidarr_ui::root_folders::RootFoldersUi; +use crate::ui::lidarr_ui::system::SystemUi; use crate::{ app::App, logos::LIDARR_LOGO, @@ -43,11 +44,12 @@ mod history; mod indexers; mod library; mod lidarr_ui_utils; +mod root_folders; +mod system; #[cfg(test)] #[path = "lidarr_ui_tests.rs"] mod lidarr_ui_tests; -mod root_folders; pub(super) struct LidarrUi; @@ -66,6 +68,7 @@ impl DrawUi for LidarrUi { _ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area), _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), _ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area), + _ if SystemUi::accepts(route) => SystemUi::draw(f, app, content_area), _ => (), } } diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap index 18499bc..ce25be8 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ Indexers │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │ │=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap index 7c6c9f9..33c20a2 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ Indexers │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Title Percent Complete Size Output Path Indexer Download Client │ │=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap index b3fced7..74dd954 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ Indexers │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Source Title ▼ Event Type Quality Date │ │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap index 94edd2c..b91005e 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ Indexers │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Indexer RSS Automatic Search Interactive Search Priority Tags │ │=> Test Indexer Enabled Enabled Enabled 25 alex │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap index 08f841b..04ab516 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ Indexers │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Path Free Space Unmapped Folders │ │=> /nfs 204800.00 GB 0 │ diff --git a/src/ui/lidarr_ui/system/mod.rs b/src/ui/lidarr_ui/system/mod.rs new file mode 100644 index 0000000..70e89f1 --- /dev/null +++ b/src/ui/lidarr_ui/system/mod.rs @@ -0,0 +1,204 @@ +use crate::ui::styles::default_style; +use std::ops::Sub; + +#[cfg(test)] +use crate::ui::ui_test_utils::test_utils::Utc; +#[cfg(not(test))] +use chrono::Utc; +use ratatui::layout::Layout; +use ratatui::text::{Span, Text}; +use ratatui::widgets::{Cell, Row}; +use ratatui::{ + Frame, + layout::{Constraint, Rect}, + widgets::ListItem, +}; + +use crate::app::App; +use crate::models::lidarr_models::LidarrTask; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::models::servarr_models::QueueEvent; +use crate::ui::lidarr_ui::system::system_details_ui::SystemDetailsUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{convert_to_minutes_hours_days, style_log_list_item}; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::{ + models::Route, + ui::{DrawUi, utils::title_block}, +}; + +mod system_details_ui; + +#[cfg(test)] +#[path = "system_ui_tests.rs"] +mod system_ui_tests; + +pub(super) const TASK_TABLE_HEADERS: [&str; 4] = + ["Name", "Interval", "Last Execution", "Next Execution"]; + +pub(super) const TASK_TABLE_CONSTRAINTS: [Constraint; 4] = [ + Constraint::Percentage(30), + Constraint::Percentage(23), + Constraint::Percentage(23), + Constraint::Percentage(23), +]; + +pub(super) struct SystemUi; + +impl DrawUi for SystemUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + SystemDetailsUi::accepts(route) || active_lidarr_block == ActiveLidarrBlock::System + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + draw_system_ui_layout(f, app, area); + + if SystemDetailsUi::accepts(route) { + SystemDetailsUi::draw(f, app, area); + } + } +} + +fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let [activities_area, logs_area] = + Layout::vertical([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(area); + + let [tasks_area, events_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(activities_area); + + draw_tasks(f, app, tasks_area); + draw_queued_events(f, app, events_area); + draw_logs(f, app, logs_area); +} + +fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let tasks_row_mapping = |task: &LidarrTask| { + let task_props = extract_task_props(task); + + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.next_execution), + ]) + .primary() + }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.lidarr_data.tasks), tasks_row_mapping) + .block(title_block("Tasks")) + .loading(app.is_loading) + .highlight_rows(false) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + + f.render_widget(tasks_table, area); +} + +pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let events_row_mapping = |event: &QueueEvent| { + let queued = convert_to_minutes_hours_days(Utc::now().sub(event.queued).num_minutes()); + let queued_string = if queued != "now" { + format!("{queued} ago") + } else { + queued + }; + let started_string = if event.started.is_some() { + let started = + convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes()); + + if started != "now" { + format!("{started} ago") + } else { + started + } + } else { + String::new() + }; + + let duration = if event.duration.is_some() { + &event.duration.as_ref().unwrap()[..8] + } else { + "" + }; + + Row::new(vec![ + Cell::from(event.trigger.clone()), + Cell::from(event.status.clone()), + Cell::from(event.command_name.clone()), + Cell::from(queued_string), + Cell::from(started_string), + Cell::from(duration.to_owned()), + ]) + .primary() + }; + let events_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.queued_events), + events_row_mapping, + ) + .block(title_block("Queued Events")) + .loading(app.is_loading) + .highlight_rows(false) + .headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"]) + .constraints([ + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(30), + Constraint::Percentage(16), + Constraint::Percentage(14), + Constraint::Percentage(14), + ]); + + f.render_widget(events_table, area); +} + +fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block("Logs"); + + if app.data.lidarr_data.logs.items.is_empty() { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + return; + } + + let logs_box = SelectableList::new(&mut app.data.lidarr_data.logs, |log| { + let log_line = log.to_string(); + let level = log_line.split('|').collect::>()[1].to_string(); + + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) + }) + .block(block) + .highlight_style(default_style()); + + f.render_widget(logs_box, area); +} + +pub(super) struct TaskProps { + pub(super) name: String, + pub(super) interval: String, + pub(super) last_execution: String, + pub(super) next_execution: String, +} + +pub(super) fn extract_task_props(task: &LidarrTask) -> TaskProps { + let interval = convert_to_minutes_hours_days(task.interval); + let next_execution = + convert_to_minutes_hours_days((task.next_execution - Utc::now()).num_minutes()); + let last_execution = + convert_to_minutes_hours_days((Utc::now() - task.last_execution).num_minutes()); + let last_execution_string = if last_execution != "now" { + format!("{last_execution} ago") + } else { + last_execution + }; + + TaskProps { + name: task.name.clone(), + interval, + last_execution: last_execution_string, + next_execution, + } +} diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemLogs.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemLogs.snap new file mode 100644 index 0000000..1781cc6 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemLogs.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Log Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemQueuedEvents.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemQueuedEvents.snap new file mode 100644 index 0000000..2b79fe3 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemQueuedEvents.snap @@ -0,0 +1,44 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + + + + + ╭ Queued Events ────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemTasks.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemTasks.snap new file mode 100644 index 0000000..4d3c667 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemTasks.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemLogs.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemLogs.snap new file mode 100644 index 0000000..a465625 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemLogs.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Log Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemQueuedEvents.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemQueuedEvents.snap new file mode 100644 index 0000000..d3f6c60 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemQueuedEvents.snap @@ -0,0 +1,44 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + + + + + ╭ Queued Events ────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemTasks.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemTasks.snap new file mode 100644 index 0000000..5f810a9 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemTasks.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemUpdates.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemUpdates.snap new file mode 100644 index 0000000..e51c393 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemUpdates.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Updates ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemLogs.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemLogs.snap new file mode 100644 index 0000000..c98c968 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemLogs.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Log Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemQueuedEvents.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemQueuedEvents.snap new file mode 100644 index 0000000..d469187 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemQueuedEvents.snap @@ -0,0 +1,44 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + + + + + ╭ Queued Events ────────────────────────────────────────────────────────────────────────────────╮ + │Trigger Status Name Queued Started Duration │ + │manual completed Refresh Monitored Downlo 4 minutes ago 4 minutes ago 00:03:03 │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTaskStartConfirmPrompt.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTaskStartConfirmPrompt.snap new file mode 100644 index 0000000..bfb2655 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTaskStartConfirmPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Name Interval Last Execution Next Execution │ + │=> Backup 1 hour now 59 minutes │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭────────────────────── Start Task ───────────────────────╮ │ + │ │ Do you want to manually start this task: Backup? │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭────────────────────────────╮╭───────────────────────────╮│ │ + │ ││ Yes ││ No ││ │ + │ │╰────────────────────────────╯╰───────────────────────────╯│ │ + │ ╰───────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTasks.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTasks.snap new file mode 100644 index 0000000..231d62e --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTasks.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Name Interval Last Execution Next Execution │ + │=> Backup 1 hour now 59 minutes │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemUpdates.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemUpdates.snap new file mode 100644 index 0000000..c5585a1 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemUpdates.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Updates ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │The latest version of Sonarr is already installed │ + │ │ + │4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed) │ + │--------------------------------------------------------------------------------------------------------------------------│ + │New: │ + │ * Cool new thing │ + │Fixed: │ + │ * Some bugs killed │ + │ │ + │ │ + │3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed) │ + │--------------------------------------------------------------------------------------------------------------------------│ + │New: │ + │ * Cool new thing (old) │ + │ * Other cool new thing (old) │ + │ │ + │ │ + │2.1.0 - 2023-04-15 02:02:53 UTC │ + │--------------------------------------------------------------------------------------------------------------------------│ + │Fixed: │ + │ * Killed bug 1 │ + │ * Fixed bug 2 │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__system_details_ui_updates_popup_loading_when_empty.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__system_details_ui_updates_popup_loading_when_empty.snap new file mode 100644 index 0000000..e51c393 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__system_details_ui_updates_popup_loading_when_empty.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Updates ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemLogs.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemLogs.snap new file mode 100644 index 0000000..dcadb23 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemLogs.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Log Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +╰───────────────────│ │────────────────────╯ +╭ Logs ───────────│ │────────────────────╮ +│2025-12-16 16:40:59│ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemQueuedEvents.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemQueuedEvents.snap new file mode 100644 index 0000000..4e39a5a --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemQueuedEvents.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Queued Events ────────────────────────────────────────────────────────────────────────────────╮ │ +│ │Trigger Status Name Queued Started Duration │ │ +│ │manual completed Refresh Monitored Downlo 4 minutes ago 4 minutes ago 00:03:03 │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +╰────────────────────────────────│ │────────────────────────────────╯ +╭ Logs ────────────────────────│ │────────────────────────────────╮ +│2025-12-16 16:40:59 UTC|INFO|Imp│ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTaskStartConfirmPrompt.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTaskStartConfirmPrompt.snap new file mode 100644 index 0000000..c0c6907 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTaskStartConfirmPrompt.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Name Interval Last Execution Next Execution │ │ +│ │=> Backup 1 hour now 59 minutes │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ ╭────────────────────── Start Task ───────────────────────╮ │ │ +│ │ │ Do you want to manually start this task: Backup? │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +╰───────────────────│ │ │ │────────────────────╯ +╭ Logs ───────────│ │ │ │────────────────────╮ +│2025-12-16 16:40:59│ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │╭────────────────────────────╮╭───────────────────────────╮│ │ │ +│ │ ││ Yes ││ No ││ │ │ +│ │ │╰────────────────────────────╯╰───────────────────────────╯│ │ │ +│ │ ╰───────────────────────────────────────────────────────────╯ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTasks.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTasks.snap new file mode 100644 index 0000000..db69cb7 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTasks.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Name Interval Last Execution Next Execution │ │ +│ │=> Backup 1 hour now 59 minutes │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +╰───────────────────│ │────────────────────╯ +╭ Logs ───────────│ │────────────────────╮ +│2025-12-16 16:40:59│ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemUpdates.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemUpdates.snap new file mode 100644 index 0000000..935bdcb --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemUpdates.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Updates ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │The latest version of Sonarr is already installed │ │ +│ │ │ │ +│ │4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed) │ │ +│ │--------------------------------------------------------------------------------------------------------------------------│ │ +│ │New: │ │ +│ │ * Cool new thing │ │ +│ │Fixed: │ │ +│ │ * Some bugs killed │ │ +│ │ │ │ +│ │ │ │ +│ │3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed) │ │ +│ │--------------------------------------------------------------------------------------------------------------------------│ │ +│ │New: │ │ +│ │ * Cool new thing (old) │ │ +│ │ * Other cool new thing (old) │ │ +│ │ │ │ +│ │ │ │ +╰───────────────────│2.1.0 - 2023-04-15 02:02:53 UTC │────────────────────╯ +╭ Logs ───────────│--------------------------------------------------------------------------------------------------------------------------│────────────────────╮ +│2025-12-16 16:40:59│Fixed: │ │ +│ │ * Killed bug 1 │ │ +│ │ * Fixed bug 2 │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_logs_loading.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_logs_loading.snap new file mode 100644 index 0000000..b6c7090 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_logs_loading.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│ ││ │ +│ ││ │ +│ Loading ... ││ Loading ... │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ │ +│ Loading ... │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab.snap new file mode 100644 index 0000000..b7f4872 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_empty.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_empty.snap new file mode 100644 index 0000000..1bf7297 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_empty.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_loading.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_loading.snap new file mode 100644 index 0000000..b6c7090 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_loading.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│ ││ │ +│ ││ │ +│ Loading ... ││ Loading ... │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ │ +│ Loading ... │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_task_and_events_loading.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_task_and_events_loading.snap new file mode 100644 index 0000000..19eb0cd --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_task_and_events_loading.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│ ││ │ +│ ││ │ +│ Loading ... ││ Loading ... │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/system_details_ui.rs b/src/ui/lidarr_ui/system/system_details_ui.rs new file mode 100644 index 0000000..0af1350 --- /dev/null +++ b/src/ui/lidarr_ui/system/system_details_ui.rs @@ -0,0 +1,144 @@ +use ratatui::Frame; +use ratatui::layout::Rect; +use ratatui::text::{Span, Text}; +use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; + +use crate::app::App; +use crate::models::Route; +use crate::models::lidarr_models::LidarrTask; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS}; +use crate::ui::lidarr_ui::system::{ + TASK_TABLE_CONSTRAINTS, TASK_TABLE_HEADERS, draw_queued_events, extract_task_props, +}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{borderless_block, style_log_list_item, title_block}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{DrawUi, draw_popup}; + +#[cfg(test)] +#[path = "system_details_ui_tests.rs"] +mod system_details_ui_tests; + +pub(super) struct SystemDetailsUi; + +impl DrawUi for SystemDetailsUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + SYSTEM_DETAILS_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + match active_lidarr_block { + ActiveLidarrBlock::SystemLogs => { + draw_logs_popup(f, app); + } + ActiveLidarrBlock::SystemTasks | ActiveLidarrBlock::SystemTaskStartConfirmPrompt => { + draw_popup(f, app, draw_tasks_popup, Size::Large) + } + ActiveLidarrBlock::SystemQueuedEvents => { + draw_popup(f, app, draw_queued_events, Size::Medium) + } + ActiveLidarrBlock::SystemUpdates => { + draw_updates_popup(f, app); + } + _ => (), + } + } + } +} + +fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let block = title_block("Log Details"); + + if app.data.lidarr_data.log_details.items.is_empty() { + let loading = LoadingBlock::new(app.is_loading, borderless_block()); + let popup = Popup::new(loading).size(Size::Large).block(block).margin(1); + + f.render_widget(popup, f.area()); + return; + } + + let logs_list = SelectableList::new(&mut app.data.lidarr_data.log_details, |log| { + let log_line = log.to_string(); + let level = log.text.split('|').collect::>()[1].to_string(); + + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) + }) + .block(borderless_block()); + let popup = Popup::new(logs_list) + .size(Size::Large) + .block(block) + .margin(1); + + f.render_widget(popup, f.area()); +} + +fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let tasks_row_mapping = |task: &LidarrTask| { + let task_props = extract_task_props(task); + + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.next_execution), + ]) + .primary() + }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.lidarr_data.tasks), tasks_row_mapping) + .loading(app.is_loading) + .margin(1) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + + f.render_widget(title_block("Tasks"), area); + f.render_widget(tasks_table, area); + + if matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::SystemTaskStartConfirmPrompt, _) + ) { + let prompt = format!( + "Do you want to manually start this task: {}?", + app.data.lidarr_data.tasks.current_selection().name + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Start Task") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } +} + +fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let updates = app.data.lidarr_data.updates.get_text(); + let block = title_block("Updates"); + + if !updates.is_empty() && !app.is_loading { + let updates_paragraph = Paragraph::new(Text::from(updates)) + .block(borderless_block()) + .scroll((app.data.lidarr_data.updates.offset, 0)); + let popup = Popup::new(updates_paragraph) + .size(Size::Large) + .block(block) + .margin(1); + + f.render_widget(popup, f.area()); + } else { + let loading = LoadingBlock::new(app.is_loading, borderless_block()); + let popup = Popup::new(loading).size(Size::Large).block(block).margin(1); + + f.render_widget(popup, f.area()); + } +} diff --git a/src/ui/lidarr_ui/system/system_details_ui_tests.rs b/src/ui/lidarr_ui/system/system_details_ui_tests.rs new file mode 100644 index 0000000..a3fafc1 --- /dev/null +++ b/src/ui/lidarr_ui/system/system_details_ui_tests.rs @@ -0,0 +1,106 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::system::system_details_ui::SystemDetailsUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_system_details_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if SYSTEM_DETAILS_BLOCKS.contains(&active_lidarr_block) { + assert!(SystemDetailsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!SystemDetailsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use super::*; + use crate::models::ScrollableText; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + #[rstest] + fn test_system_details_ui_popups( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("popup_{active_lidarr_block}"), output); + } + + #[rstest] + fn test_system_details_ui_loading( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("loading_{active_lidarr_block}"), output); + } + + #[test] + fn test_system_details_ui_updates_popup_loading_when_empty() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::SystemUpdates.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("".to_string()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_system_details_ui_popups_empty( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("empty_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/system/system_ui_tests.rs b/src/ui/lidarr_ui/system/system_ui_tests.rs new file mode 100644 index 0000000..58d9fcf --- /dev/null +++ b/src/ui/lidarr_ui/system/system_ui_tests.rs @@ -0,0 +1,123 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::system::SystemUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_system_ui_accepts() { + let mut system_ui_blocks = Vec::new(); + system_ui_blocks.push(ActiveLidarrBlock::System); + system_ui_blocks.extend(SYSTEM_DETAILS_BLOCKS); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if system_ui_blocks.contains(&active_lidarr_block) { + assert!(SystemUi::accepts(active_lidarr_block.into())); + } else { + assert!(!SystemUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::models::stateful_list::StatefulList; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_system_ui_renders_system_tab_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_system_ui_renders_logs_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs = StatefulList::default(); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_system_ui_renders_system_tab_task_and_events_loading() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_system_ui_renders_system_tab() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_system_ui_renders_system_tab_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_system_details_ui_renders_popups_over_system_ui( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("popups_over_system_ui_{active_lidarr_block}"), + output + ); + } + } +}