diff --git a/Cargo.toml b/Cargo.toml index 88da2d0..8bb63a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.0.17" +version = "0.0.18" authors = ["Alex Clarke "] description = "A TUI for managing *arr servers" keywords = ["managarr", "tui-rs", "dashboard", "servarr"] diff --git a/README.md b/README.md index 8f9d82b..5251c8b 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ tautulli: - [ ] Manage your quality profiles - [ ] Manage your quality definitions - [ ] Manage your indexers +- [x] View and browse logs, tasks, and events queues +- [x] Manually trigger scheduled tasks ### Sonarr - [ ] Support for Sonarr @@ -119,7 +121,7 @@ tautulli: - [ ] Support for Tautulli ## Dependencies -* [tui-rs](https://github.com/fdehau/tui-rs) +* [ratatui](https://github.com/tui-rs-revival/ratatui) * [crossterm](https://github.com/crossterm-rs/crossterm) * [clap](https://github.com/clap-rs/clap) * [tokio](https://github.com/tokio-rs/tokio) diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index 0124769..5027fbf 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -19,6 +19,8 @@ generate_keybindings! { filter, sort, edit, + logs, + tasks, refresh, update, home, @@ -75,6 +77,14 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::Char('e'), desc: "Edit", }, + logs: KeyBinding { + key: Key::Char('l'), + desc: "Logs", + }, + tasks: KeyBinding { + key: Key::Char('t'), + desc: "Tasks", + }, refresh: KeyBinding { key: Key::Char('r'), desc: "Refresh", diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 9115b58..54a0304 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -4,8 +4,9 @@ use strum::IntoEnumIterator; use crate::app::{App, Route}; use crate::models::radarr_models::{ - AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Event, Log, - MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release, ReleaseField, RootFolder, Task, + AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, + MinimumAvailability, Monitor, Movie, MovieHistoryItem, QueueEvent, Release, ReleaseField, + RootFolder, Task, }; use crate::models::{ BlockSelectionState, HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable, @@ -49,9 +50,10 @@ pub struct RadarrData<'a> { pub collections: StatefulTable, pub filtered_collections: StatefulTable, pub collection_movies: StatefulTable, - pub logs: StatefulList, + pub logs: StatefulList, + pub log_details: StatefulList, pub tasks: StatefulTable, - pub events: StatefulTable, + pub queued_events: StatefulTable, pub prompt_confirm_action: Option, pub main_tabs: TabState, pub movie_info_tabs: TabState, @@ -74,6 +76,10 @@ impl<'a> RadarrData<'a> { self.collection_movies = StatefulTable::default(); } + pub fn reset_log_details_list(&mut self) { + self.log_details = StatefulList::default(); + } + pub fn reset_delete_movie_preferences(&mut self) { self.delete_movie_files = false; self.add_list_exclusion = false; @@ -269,8 +275,9 @@ impl<'a> Default for RadarrData<'a> { filtered_collections: StatefulTable::default(), collection_movies: StatefulTable::default(), logs: StatefulList::default(), + log_details: StatefulList::default(), tasks: StatefulTable::default(), - events: StatefulTable::default(), + queued_events: StatefulTable::default(), prompt_confirm_action: None, search: HorizontallyScrollableText::default(), filter: HorizontallyScrollableText::default(), @@ -313,7 +320,7 @@ impl<'a> Default for RadarrData<'a> { title: "System", route: ActiveRadarrBlock::System.into(), help: "", - contextual_help: Some(" open tasks | open queue | open logs") + contextual_help: Some(" open tasks | open queue | open logs | refresh") } ]), movie_info_tabs: TabState::new(vec![ @@ -410,6 +417,10 @@ pub enum ActiveRadarrBlock { Movies, RootFolders, System, + SystemLogs, + SystemTasks, + SystemTaskStartConfirmPrompt, + SystemQueue, UpdateAndScanPrompt, UpdateAllCollectionsPrompt, UpdateAllMoviesPrompt, @@ -508,6 +519,12 @@ pub static DELETE_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 3] = [ ActiveRadarrBlock::DeleteMovieToggleAddListExclusion, ActiveRadarrBlock::DeleteMovieConfirmPrompt, ]; +pub static SYSTEM_DETAILS_BLOCKS: [ActiveRadarrBlock; 4] = [ + ActiveRadarrBlock::SystemLogs, + ActiveRadarrBlock::SystemTasks, + ActiveRadarrBlock::SystemQueue, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt, +]; impl From for Route { fn from(active_radarr_block: ActiveRadarrBlock) -> Route { @@ -557,7 +574,7 @@ impl<'a> App<'a> { .dispatch_network_event(RadarrEvent::GetTasks.into()) .await; self - .dispatch_network_event(RadarrEvent::GetEvents.into()) + .dispatch_network_event(RadarrEvent::GetQueuedEvents.into()) .await; self .dispatch_network_event(RadarrEvent::GetLogs.into()) diff --git a/src/app/radarr_test_utils.rs b/src/app/radarr_test_utils.rs index 5555ee5..c16873c 100644 --- a/src/app/radarr_test_utils.rs +++ b/src/app/radarr_test_utils.rs @@ -2,10 +2,10 @@ pub mod utils { use crate::app::radarr::RadarrData; use crate::models::radarr_models::{ - AddMovieSearchResult, Collection, CollectionMovie, Credit, Log, MinimumAvailability, Monitor, - Movie, MovieHistoryItem, Release, ReleaseField, RootFolder, + AddMovieSearchResult, Collection, CollectionMovie, Credit, MinimumAvailability, Monitor, Movie, + MovieHistoryItem, Release, ReleaseField, RootFolder, }; - use crate::models::ScrollableText; + use crate::models::{HorizontallyScrollableText, ScrollableText}; pub fn create_test_radarr_data<'a>() -> RadarrData<'a> { let mut radarr_data = RadarrData { @@ -60,7 +60,9 @@ pub mod utils { radarr_data .collection_movies .set_items(vec![CollectionMovie::default()]); - radarr_data.logs.set_items(vec![Log::default()]); + radarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); radarr_data } diff --git a/src/app/radarr_tests.rs b/src/app/radarr_tests.rs index 207e75f..087fadf 100644 --- a/src/app/radarr_tests.rs +++ b/src/app/radarr_tests.rs @@ -44,6 +44,15 @@ mod tests { assert!(radarr_data.collection_movies.items.is_empty()); } + #[test] + fn test_reset_log_details_list() { + let mut radarr_data = create_test_radarr_data(); + + radarr_data.reset_log_details_list(); + + assert!(radarr_data.log_details.items.is_empty()); + } + #[test] fn test_reset_delete_movie_preferences() { let mut radarr_data = create_test_radarr_data(); @@ -279,8 +288,9 @@ mod tests { assert!(radarr_data.filtered_collections.items.is_empty()); assert!(radarr_data.collection_movies.items.is_empty()); assert!(radarr_data.logs.items.is_empty()); + assert!(radarr_data.log_details.items.is_empty()); assert!(radarr_data.tasks.items.is_empty()); - assert!(radarr_data.events.items.is_empty()); + assert!(radarr_data.queued_events.items.is_empty()); assert!(radarr_data.prompt_confirm_action.is_none()); assert!(radarr_data.search.text.is_empty()); assert!(radarr_data.filter.text.is_empty()); @@ -345,7 +355,7 @@ mod tests { assert!(radarr_data.main_tabs.tabs[4].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[4].contextual_help, - Some(" open tasks | open queue | open logs") + Some(" open tasks | open queue | open logs | refresh") ); assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6); @@ -688,7 +698,7 @@ mod tests { ); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetEvents.into() + RadarrEvent::GetQueuedEvents.into() ); assert_eq!( sync_network_rx.recv().await.unwrap(), diff --git a/src/handlers/handler_test_utils.rs b/src/handlers/handler_test_utils.rs index 2f20265..31abf61 100644 --- a/src/handlers/handler_test_utils.rs +++ b/src/handlers/handler_test_utils.rs @@ -15,6 +15,7 @@ mod test_utils { }, ] }; + ($name:ident, $title_ident:ident) => { vec![ $name { @@ -27,6 +28,7 @@ mod test_utils { }, ] }; + ($name:ident, $title_ident:ident, $field:ident) => { vec![ $name { @@ -59,6 +61,7 @@ mod test_utils { }, ] }; + ($name:ident, $title_ident:ident) => { vec![ $name { @@ -75,6 +78,7 @@ mod test_utils { }, ] }; + ($name:ident, $title_ident:ident, $field:ident) => { vec![ $name { @@ -114,6 +118,7 @@ mod test_utils { assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 1"); } }; + ($func:ident, $handler:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { @@ -139,6 +144,7 @@ mod test_utils { ); } }; + ($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { @@ -160,6 +166,7 @@ mod test_utils { ); } }; + ($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { @@ -252,6 +259,7 @@ mod test_utils { assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 1"); } }; + ($func:ident, $handler:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => { #[test] fn $func() { @@ -277,6 +285,7 @@ mod test_utils { ); } }; + ($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => { #[test] fn $func() { @@ -298,6 +307,7 @@ mod test_utils { ); } }; + ($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => { #[test] fn $func() { diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 69182c3..59f09af 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -3,7 +3,7 @@ use crate::app::radarr::{ ActiveRadarrBlock, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, DELETE_MOVIE_SELECTION_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, FILTER_BLOCKS, MOVIE_DETAILS_BLOCKS, - SEARCH_BLOCKS, + SEARCH_BLOCKS, SYSTEM_DETAILS_BLOCKS, }; use crate::handlers::radarr_handlers::add_movie_handler::AddMovieHandler; use crate::handlers::radarr_handlers::collection_details_handler::CollectionDetailsHandler; @@ -11,6 +11,7 @@ use crate::handlers::radarr_handlers::delete_movie_handler::DeleteMovieHandler; use crate::handlers::radarr_handlers::edit_collection_handler::EditCollectionHandler; use crate::handlers::radarr_handlers::edit_movie_handler::EditMovieHandler; use crate::handlers::radarr_handlers::movie_details_handler::MovieDetailsHandler; +use crate::handlers::radarr_handlers::system_details_handler::SystemDetailsHandler; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable}; use crate::network::radarr_network::RadarrEvent; @@ -23,6 +24,7 @@ mod delete_movie_handler; mod edit_collection_handler; mod edit_movie_handler; mod movie_details_handler; +mod system_details_handler; #[cfg(test)] #[path = "radarr_handler_tests.rs"] @@ -64,6 +66,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b EditCollectionHandler::with(self.key, self.app, self.active_radarr_block, self.context) .handle() } + _ if SYSTEM_DETAILS_BLOCKS.contains(self.active_radarr_block) => { + SystemDetailsHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle(); + } _ => self.handle_key_event(), } } @@ -564,6 +570,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b } _ => (), }, + ActiveRadarrBlock::System => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ if *key == DEFAULT_KEYBINDINGS.update.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::SystemQueue.into()); + } + _ if *key == DEFAULT_KEYBINDINGS.logs.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::SystemLogs.into()); + self + .app + .data + .radarr_data + .log_details + .set_items(self.app.data.radarr_data.logs.items.to_vec()); + self.app.data.radarr_data.log_details.scroll_to_bottom(); + } + _ if *key == DEFAULT_KEYBINDINGS.tasks.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); + } + _ => (), + }, ActiveRadarrBlock::AddRootFolderPrompt => { handle_text_box_keys!(self, key, self.app.data.radarr_data.edit_path) } diff --git a/src/handlers/radarr_handlers/movie_details_handler_tests.rs b/src/handlers/radarr_handlers/movie_details_handler_tests.rs index e707083..b5bce19 100644 --- a/src/handlers/radarr_handlers/movie_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/movie_details_handler_tests.rs @@ -625,7 +625,7 @@ mod tests { let release_a = Release { protocol: "Protocol A".to_owned(), age: Number::from(1), - title: HorizontallyScrollableText::from("Title A".to_owned()), + title: HorizontallyScrollableText::from("Title A"), indexer: "Indexer A".to_owned(), size: Number::from(1), rejected: true, @@ -643,7 +643,7 @@ mod tests { let release_b = Release { protocol: "Protocol B".to_owned(), age: Number::from(2), - title: HorizontallyScrollableText::from("Title B".to_owned()), + title: HorizontallyScrollableText::from("Title B"), indexer: "Indexer B".to_owned(), size: Number::from(2), rejected: false, @@ -661,7 +661,7 @@ mod tests { let release_c = Release { protocol: "Protocol C".to_owned(), age: Number::from(3), - title: HorizontallyScrollableText::from("Title C".to_owned()), + title: HorizontallyScrollableText::from("Title C"), indexer: "Indexer C".to_owned(), size: Number::from(3), rejected: false, diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index 240a83d..cf43f18 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -754,7 +754,7 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into()); - app.data.radarr_data.edit_path = HorizontallyScrollableText::from("/nfs/test".to_owned()); + app.data.radarr_data.edit_path = HorizontallyScrollableText::from("/nfs/test"); app.should_ignore_quit_key = true; RadarrHandler::with( @@ -921,6 +921,7 @@ mod tests { ActiveRadarrBlock::Collections, ActiveRadarrBlock::UpdateAllCollectionsPrompt )] + #[case(ActiveRadarrBlock::System, ActiveRadarrBlock::SystemQueue)] fn test_update_key( #[case] active_radarr_block: ActiveRadarrBlock, #[case] expected_radarr_block: ActiveRadarrBlock, @@ -944,7 +945,8 @@ mod tests { ActiveRadarrBlock::Movies, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Downloads, - ActiveRadarrBlock::RootFolders + ActiveRadarrBlock::RootFolders, + ActiveRadarrBlock::System )] active_radarr_block: ActiveRadarrBlock, ) { @@ -963,6 +965,54 @@ mod tests { assert!(app.should_refresh); } + #[test] + fn test_logs_key() { + let mut app = App::default(); + app.data.radarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + + RadarrHandler::with( + &DEFAULT_KEYBINDINGS.logs.key, + &mut app, + &ActiveRadarrBlock::System, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::SystemLogs.into() + ); + assert_eq!( + app.data.radarr_data.log_details.items, + app.data.radarr_data.logs.items + ); + assert_str_eq!( + app.data.radarr_data.log_details.current_selection().text, + "test 2" + ); + } + + #[test] + fn test_tasks_key() { + let mut app = App::default(); + + RadarrHandler::with( + &DEFAULT_KEYBINDINGS.tasks.key, + &mut app, + &ActiveRadarrBlock::System, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::SystemTasks.into() + ); + } + #[test] fn test_add_root_folder_prompt_backspace_key() { let mut app = App::default(); @@ -1196,6 +1246,19 @@ mod tests { assert!(app.data.radarr_data.filter.text.is_empty()); } + #[rstest] + fn test_delegates_system_details_blocks_to_system_details_handler( + #[values( + ActiveRadarrBlock::System, + ActiveRadarrBlock::SystemLogs, + ActiveRadarrBlock::SystemTasks, + ActiveRadarrBlock::SystemQueue + )] + active_radarr_block: ActiveRadarrBlock, + ) { + test_handler_delegation!(ActiveRadarrBlock::System, active_radarr_block); + } + #[rstest] fn test_delegates_add_movie_blocks_to_add_movie_handler( #[values( diff --git a/src/handlers/radarr_handlers/system_details_handler.rs b/src/handlers/radarr_handlers/system_details_handler.rs new file mode 100644 index 0000000..0a26f2d --- /dev/null +++ b/src/handlers/radarr_handlers/system_details_handler.rs @@ -0,0 +1,150 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::radarr::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; +use crate::app::App; +use crate::event::Key; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::Scrollable; +use crate::network::radarr_network::RadarrEvent; + +#[cfg(test)] +#[path = "system_details_handler_tests.rs"] +mod system_details_handler_tests; + +pub(super) struct SystemDetailsHandler<'a, 'b> { + key: &'a Key, + app: &'a mut App<'b>, + active_radarr_block: &'a ActiveRadarrBlock, + _context: &'a Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler<'a, 'b> { + fn with( + key: &'a Key, + app: &'a mut App<'b>, + active_block: &'a ActiveRadarrBlock, + context: &'a Option, + ) -> SystemDetailsHandler<'a, 'b> { + SystemDetailsHandler { + key, + app, + active_radarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> &Key { + self.key + } + + fn handle_scroll_up(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::SystemLogs => self.app.data.radarr_data.log_details.scroll_up(), + ActiveRadarrBlock::SystemTasks => self.app.data.radarr_data.tasks.scroll_up(), + ActiveRadarrBlock::SystemQueue => self.app.data.radarr_data.queued_events.scroll_up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::SystemLogs => self.app.data.radarr_data.log_details.scroll_down(), + ActiveRadarrBlock::SystemTasks => self.app.data.radarr_data.tasks.scroll_down(), + ActiveRadarrBlock::SystemQueue => self.app.data.radarr_data.queued_events.scroll_down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::SystemLogs => self.app.data.radarr_data.log_details.scroll_to_top(), + ActiveRadarrBlock::SystemTasks => self.app.data.radarr_data.tasks.scroll_to_top(), + ActiveRadarrBlock::SystemQueue => self.app.data.radarr_data.queued_events.scroll_to_top(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::SystemLogs => self.app.data.radarr_data.log_details.scroll_to_bottom(), + ActiveRadarrBlock::SystemTasks => self.app.data.radarr_data.tasks.scroll_to_bottom(), + ActiveRadarrBlock::SystemQueue => self.app.data.radarr_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_radarr_block { + ActiveRadarrBlock::SystemLogs => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.left.key => { + self + .app + .data + .radarr_data + .log_details + .items + .iter() + .for_each(|log| log.scroll_right()); + } + _ if *key == DEFAULT_KEYBINDINGS.right.key => { + self + .app + .data + .radarr_data + .log_details + .items + .iter() + .for_each(|log| log.scroll_left()); + } + _ => (), + }, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::SystemTasks => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); + } + ActiveRadarrBlock::SystemTaskStartConfirmPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::SystemLogs + | ActiveRadarrBlock::SystemTasks + | ActiveRadarrBlock::SystemQueue => { + self.app.data.radarr_data.reset_log_details_list(); + self.app.pop_navigation_stack() + } + ActiveRadarrBlock::SystemTaskStartConfirmPrompt => { + self.app.pop_navigation_stack(); + self.app.data.radarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + if SYSTEM_DETAILS_BLOCKS.contains(self.active_radarr_block) + && self.key == &DEFAULT_KEYBINDINGS.refresh.key + { + self.app.should_refresh = true; + } + } +} diff --git a/src/handlers/radarr_handlers/system_details_handler_tests.rs b/src/handlers/radarr_handlers/system_details_handler_tests.rs new file mode 100644 index 0000000..1e764ea --- /dev/null +++ b/src/handlers/radarr_handlers/system_details_handler_tests.rs @@ -0,0 +1,409 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_str_eq; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::radarr::ActiveRadarrBlock; + use crate::app::App; + use crate::event::Key; + use crate::handlers::radarr_handlers::system_details_handler::SystemDetailsHandler; + use crate::handlers::KeyEventHandler; + use crate::models::radarr_models::{QueueEvent, Task}; + + mod test_handle_scroll_up_and_down { + use rstest::rstest; + + use crate::models::HorizontallyScrollableText; + use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + + use super::*; + + test_iterable_scroll!( + test_log_details_scroll, + SystemDetailsHandler, + log_details, + simple_stateful_iterable_vec!(HorizontallyScrollableText, String, text), + ActiveRadarrBlock::SystemLogs, + None, + text + ); + + test_iterable_scroll!( + test_tasks_scroll, + SystemDetailsHandler, + tasks, + simple_stateful_iterable_vec!(Task, String, name), + ActiveRadarrBlock::SystemTasks, + None, + name + ); + + test_iterable_scroll!( + test_queued_events_scroll, + SystemDetailsHandler, + queued_events, + simple_stateful_iterable_vec!(QueueEvent, String, name), + ActiveRadarrBlock::SystemQueue, + None, + name + ); + } + + mod test_handle_home_end { + use crate::models::HorizontallyScrollableText; + use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + + use super::*; + + test_iterable_home_and_end!( + test_log_details_home_end, + SystemDetailsHandler, + log_details, + extended_stateful_iterable_vec!(HorizontallyScrollableText, String, text), + ActiveRadarrBlock::SystemLogs, + None, + text + ); + + test_iterable_home_and_end!( + test_tasks_home_end, + SystemDetailsHandler, + tasks, + extended_stateful_iterable_vec!(Task, String, name), + ActiveRadarrBlock::SystemTasks, + None, + name + ); + + test_iterable_home_and_end!( + test_queued_events_home_end, + SystemDetailsHandler, + queued_events, + extended_stateful_iterable_vec!(QueueEvent, String, name), + ActiveRadarrBlock::SystemQueue, + None, + name + ); + } + + mod test_handle_left_right_action { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[test] + fn test_handle_log_details_left_right() { + let active_radarr_block = ActiveRadarrBlock::SystemLogs; + let mut app = App::default(); + app + .data + .radarr_data + .log_details + .set_items(vec!["t1".into(), "t22".into()]); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.data.radarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "t22"); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.data.radarr_data.log_details.items[0].to_string(), "1"); + assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "22"); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.data.radarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "2"); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.data.radarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), ""); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.data.radarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), ""); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.data.radarr_data.log_details.items[0].to_string(), "1"); + assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "2"); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.data.radarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "22"); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.data.radarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.radarr_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::default(); + + SystemDetailsHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + &None, + ) + .handle(); + + assert!(app.data.radarr_data.prompt_confirm); + + SystemDetailsHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + &None, + ) + .handle(); + + assert!(!app.data.radarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::network::radarr_network::RadarrEvent; + 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::default(); + + SystemDetailsHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::SystemTasks, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into() + ); + } + + #[test] + fn test_system_tasks_start_task_prompt_confirm_submit() { + let mut app = App::default(); + app.data.radarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + &None, + ) + .handle(); + + assert!(app.data.radarr_data.prompt_confirm); + assert_eq!( + app.data.radarr_data.prompt_confirm_action, + Some(RadarrEvent::StartTask) + ); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::SystemTasks.into() + ); + } + + #[test] + fn test_system_tasks_start_task_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + &None, + ) + .handle(); + + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.data.radarr_data.prompt_confirm_action, None); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::SystemTasks.into() + ); + } + } + + mod test_handle_esc { + use crate::models::HorizontallyScrollableText; + use pretty_assertions::assert_eq; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_esc_system_logs() { + let mut app = App::default(); + app + .data + .radarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::from("test")]); + app.push_navigation_stack(ActiveRadarrBlock::System.into()); + app.push_navigation_stack(ActiveRadarrBlock::SystemLogs.into()); + app + .data + .radarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); + + SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemLogs, &None) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert!(app.data.radarr_data.log_details.items.is_empty()); + } + + #[test] + fn test_esc_system_tasks() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::System.into()); + app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); + app.data.radarr_data.tasks.set_items(vec![Task::default()]); + + SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemTasks, &None) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + } + + #[test] + fn test_esc_system_queue() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::System.into()); + app.push_navigation_stack(ActiveRadarrBlock::SystemQueue.into()); + app + .data + .radarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemQueue, &None) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + } + + #[test] + fn test_system_tasks_start_task_prompt_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); + app.data.radarr_data.prompt_confirm = true; + + SystemDetailsHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::SystemTasks.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + } + } + + mod test_handle_key_char { + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_refresh_key( + #[values( + ActiveRadarrBlock::SystemLogs, + ActiveRadarrBlock::SystemTasks, + ActiveRadarrBlock::SystemQueue + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(active_radarr_block.into()); + + SystemDetailsHandler::with( + &DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + &active_radarr_block, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &active_radarr_block.into()); + assert!(app.should_refresh); + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index acf9376..36ab62f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -174,6 +174,12 @@ impl From for HorizontallyScrollableText { } } +impl From<&str> for HorizontallyScrollableText { + fn from(text: &str) -> HorizontallyScrollableText { + HorizontallyScrollableText::new(text.to_owned()) + } +} + impl Display for HorizontallyScrollableText { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if *self.offset.borrow() == 0 { diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index ff569c2..439ce4b 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -158,7 +158,7 @@ mod tests { } #[test] - fn test_horizontally_scrollable_text_from() { + fn test_horizontally_scrollable_text_from_string() { let test_text = "Test string"; let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned()); @@ -166,10 +166,19 @@ mod tests { assert_str_eq!(horizontally_scrollable_text.text, test_text); } + #[test] + fn test_horizontally_scrollable_text_from_str() { + let test_text = "Test string"; + let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text); + + assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0); + assert_str_eq!(horizontally_scrollable_text.text, test_text); + } + #[test] fn test_horizontally_scrollable_text_to_string() { let test_text = "Test string"; - let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned()); + let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text); assert_str_eq!(horizontally_scrollable_text.to_string(), test_text); @@ -199,7 +208,7 @@ mod tests { #[test] fn test_horizontally_scrollable_text_scroll_text_left() { - let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string".to_owned()); + let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string"); assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0); @@ -219,7 +228,7 @@ mod tests { #[test] fn test_horizontally_scrollable_text_scroll_text_right() { - let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string".to_owned()); + let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string"); *horizontally_scrollable_text.offset.borrow_mut() = horizontally_scrollable_text.text.len(); for i in 1..horizontally_scrollable_text.text.len() { @@ -238,7 +247,7 @@ mod tests { #[test] fn test_horizontally_scrollable_text_scroll_home() { - let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string".to_owned()); + let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string"); horizontally_scrollable_text.scroll_home(); @@ -264,7 +273,7 @@ mod tests { fn test_horizontally_scrollable_text_scroll_or_reset() { let width = 3; let test_text = "Test string"; - let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned()); + let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text); horizontally_scrollable_text.scroll_left_or_reset(width, true, true); @@ -289,7 +298,7 @@ mod tests { #[test] fn test_horizontally_scrollable_test_scroll_or_reset_resets_when_text_unselected() { - let horizontally_scrollable_test = HorizontallyScrollableText::from("Test string".to_owned()); + let horizontally_scrollable_test = HorizontallyScrollableText::from("Test string"); horizontally_scrollable_test.scroll_left(); assert_eq!(*horizontally_scrollable_test.offset.borrow(), 1); @@ -302,7 +311,7 @@ mod tests { #[test] fn test_horizontally_scrollable_text_drain() { let test_text = "Test string"; - let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned()); + let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text); assert_str_eq!(horizontally_scrollable_text.drain(), test_text); assert!(horizontally_scrollable_text.text.is_empty()); @@ -312,7 +321,7 @@ mod tests { #[test] fn test_horizontally_scrollable_text_pop() { let test_text = "Test string"; - let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned()); + let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text); horizontally_scrollable_text.pop(); assert_str_eq!(horizontally_scrollable_text.text, "Test strin"); @@ -341,7 +350,7 @@ mod tests { #[test] fn test_horizontally_scrollable_text_push() { let test_text = "Test string"; - let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned()); + let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text); horizontally_scrollable_text.push('h'); assert_str_eq!(horizontally_scrollable_text.text, "Test stringh"); diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index a8b765a..330ea4b 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -414,7 +414,7 @@ pub struct Log { pub time: DateTime, pub exception: Option, pub exception_type: Option, - pub level: Option, + pub level: String, pub logger: Option, pub message: Option, pub method: Option, @@ -440,7 +440,7 @@ pub struct Task { #[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -pub struct Event { +pub struct QueueEvent { pub trigger: String, pub name: String, pub command_name: String, @@ -448,5 +448,5 @@ pub struct Event { pub queued: DateTime, pub started: Option>, pub ended: Option>, - pub duration: String, + pub duration: Option, } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 5d814a6..7c0c1bb 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -10,11 +10,11 @@ use crate::app::radarr::ActiveRadarrBlock; use crate::app::RadarrConfig; use crate::models::radarr_models::{ AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie, - CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Event, - LogResponse, Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, Release, + CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, LogResponse, + Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, }; -use crate::models::{Route, Scrollable, ScrollableText}; +use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText}; use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps}; use crate::utils::{convert_runtime, convert_to_gb}; @@ -34,7 +34,7 @@ pub enum RadarrEvent { EditCollection, GetCollections, GetDownloads, - GetEvents, + GetQueuedEvents, GetLogs, GetMovieCredits, GetMovieDetails, @@ -49,6 +49,7 @@ pub enum RadarrEvent { GetTasks, HealthCheck, SearchNewMovie, + StartTask, TriggerAutomaticSearch, UpdateAllMovies, UpdateAndScan, @@ -79,7 +80,8 @@ impl RadarrEvent { RadarrEvent::GetStatus => "/system/status", RadarrEvent::GetTags => "/tag", RadarrEvent::GetTasks => "/system/task", - RadarrEvent::GetEvents + RadarrEvent::StartTask + | RadarrEvent::GetQueuedEvents | RadarrEvent::TriggerAutomaticSearch | RadarrEvent::UpdateAndScan | RadarrEvent::UpdateAllMovies @@ -109,7 +111,7 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::EditCollection => self.edit_collection().await, RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetDownloads => self.get_downloads().await, - RadarrEvent::GetEvents => self.get_events().await, + RadarrEvent::GetQueuedEvents => self.get_queued_events().await, RadarrEvent::GetLogs => self.get_logs().await, RadarrEvent::GetMovieCredits => self.get_credits().await, RadarrEvent::GetMovieDetails => self.get_movie_details().await, @@ -124,6 +126,7 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::GetTasks => self.get_tasks().await, RadarrEvent::HealthCheck => self.get_healthcheck().await, RadarrEvent::SearchNewMovie => self.search_movie().await, + RadarrEvent::StartTask => self.start_task().await, RadarrEvent::TriggerAutomaticSearch => self.trigger_automatic_search().await, RadarrEvent::UpdateAllMovies => self.update_all_movies().await, RadarrEvent::UpdateAndScan => self.update_and_scan().await, @@ -280,6 +283,35 @@ impl<'a, 'b> Network<'a, 'b> { .await; } + async fn start_task(&self) { + let task_name = self + .app + .lock() + .await + .data + .radarr_data + .tasks + .current_selection() + .task_name + .clone(); + + info!("Starting Radarr task: {}", task_name); + + let body = CommandBody { name: task_name }; + + let request_props = self + .radarr_request_props_from( + RadarrEvent::StartTask.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + async fn update_and_scan(&self) { let movie_id = self.extract_movie_id().await; info!("Updating and scanning movie with id: {}", movie_id); @@ -564,7 +596,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Fetching Radarr logs"); let resource = format!( - "{}?pageSize=1000&sortDirection=descending&sortKey=time", + "{}?pageSize=100&sortDirection=descending&sortKey=time", RadarrEvent::GetLogs.resource() ); let request_props = self @@ -576,7 +608,31 @@ impl<'a, 'b> Network<'a, 'b> { let mut logs = log_response.records; logs.reverse(); - app.data.radarr_data.logs.set_items(logs); + 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().unwrap(), + log.exception_type.as_ref().unwrap(), + log.exception.as_ref().unwrap() + )) + } else { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log.logger.as_ref().unwrap(), + log.message.as_ref().unwrap() + )) + } + }) + .collect(); + + app.data.radarr_data.logs.set_items(log_lines); app.data.radarr_data.logs.scroll_to_bottom(); }) .await; @@ -604,20 +660,24 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_events(&self) { - info!("Fetching Radarr events"); + async fn get_queued_events(&self) { + info!("Fetching Radarr queued events"); let request_props = self .radarr_request_props_from( - RadarrEvent::GetEvents.resource(), + RadarrEvent::GetQueuedEvents.resource(), RequestMethod::Get, None::<()>, ) .await; self - .handle_request::<(), Vec>(request_props, |events_vec, mut app| { - app.data.radarr_data.events.set_items(events_vec); + .handle_request::<(), Vec>(request_props, |queued_events_vec, mut app| { + app + .data + .radarr_data + .queued_events + .set_items(queued_events_vec); }) .await; } diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index fc67a13..ddf664a 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -13,7 +13,7 @@ mod test { use crate::app::radarr::ActiveRadarrBlock; use crate::models::radarr_models::{ - CollectionMovie, Language, Log, MediaInfo, MinimumAvailability, Monitor, MovieFile, Quality, + CollectionMovie, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList, }; use crate::models::HorizontallyScrollableText; @@ -376,6 +376,34 @@ mod test { ); } + #[tokio::test] + async fn test_handle_start_task_event() { + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Post, + Some(json!({ + "name": "TestTask" + })), + None, + RadarrEvent::StartTask.resource(), + ) + .await; + app_arc + .lock() + .await + .data + .radarr_data + .tasks + .set_items(vec![Task { + task_name: "TestTask".to_owned(), + ..Task::default() + }]); + let network = Network::new(reqwest::Client::new(), &app_arc); + + network.handle_radarr_event(RadarrEvent::StartTask).await; + + async_server.assert_async().await; + } + #[tokio::test] async fn test_handle_search_new_movie_event_no_results() { let resource = format!( @@ -827,8 +855,8 @@ mod test { } #[tokio::test] - async fn test_handle_get_events_event() { - let events_json = json!([{ + async fn test_handle_get_queued_events_event() { + let queued_events_json = json!([{ "name": "RefreshMonitoredDownloads", "commandName": "Refresh Monitored Downloads", "status": "completed", @@ -839,31 +867,33 @@ mod test { "trigger": "scheduled", }]); let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); - let expected_event = Event { + 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: "00:00:00.5111547".to_owned(), + duration: Some("00:00:00.5111547".to_owned()), trigger: "scheduled".to_owned(), }; let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, - Some(events_json), - RadarrEvent::GetEvents.resource(), + Some(queued_events_json), + RadarrEvent::GetQueuedEvents.resource(), ) .await; let network = Network::new(reqwest::Client::new(), &app_arc); - network.handle_radarr_event(RadarrEvent::GetEvents).await; + network + .handle_radarr_event(RadarrEvent::GetQueuedEvents) + .await; async_server.assert_async().await; assert_eq!( - app_arc.lock().await.data.radarr_data.events.items, + app_arc.lock().await.data.radarr_data.queued_events.items, vec![expected_event] ); } @@ -871,26 +901,14 @@ mod test { #[tokio::test] async fn test_handle_get_logs_event() { let resource = format!( - "{}?pageSize=1000&sortDirection=descending&sortKey=time", + "{}?pageSize=100&sortDirection=descending&sortKey=time", RadarrEvent::GetLogs.resource() ); - let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); let expected_logs = vec![ - Log { - time: timestamp, - level: Some("fatal".to_owned()), - logger: Some("RadarrError".to_owned()), - exception: Some("test exception".to_owned()), - exception_type: Some("Some.Big.Bad.Exception".to_owned()), - ..Log::default() - }, - Log { - time: timestamp, - level: Some("info".to_owned()), - logger: Some("TestLogger".to_owned()), - message: Some("test message".to_owned()), - ..Log::default() - }, + HorizontallyScrollableText::from( + "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", + ), + HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), ]; let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, @@ -931,19 +949,15 @@ mod test { app_arc.lock().await.data.radarr_data.logs.items, expected_logs ); - assert_str_eq!( - app_arc - .lock() - .await - .data - .radarr_data - .logs - .current_selection() - .level - .as_ref() - .unwrap(), - "info" - ); + assert!(app_arc + .lock() + .await + .data + .radarr_data + .logs + .current_selection() + .text + .contains("INFO")); } #[tokio::test] @@ -1307,8 +1321,7 @@ mod test { ) .await; - app_arc.lock().await.data.radarr_data.edit_path = - HorizontallyScrollableText::from("/nfs/test".to_owned()); + app_arc.lock().await.data.radarr_data.edit_path = HorizontallyScrollableText::from("/nfs/test"); let network = Network::new(reqwest::Client::new(), &app_arc); network @@ -1937,7 +1950,7 @@ mod test { guid: "1234".to_owned(), protocol: "torrent".to_owned(), age: Number::from(1), - title: HorizontallyScrollableText::from("Test Release".to_owned()), + title: HorizontallyScrollableText::from("Test Release"), indexer: "kickass torrents".to_owned(), indexer_id: Number::from(2), size: Number::from(1234), @@ -1953,7 +1966,7 @@ mod test { fn add_movie_search_result() -> AddMovieSearchResult { AddMovieSearchResult { tmdb_id: Number::from(1234), - title: HorizontallyScrollableText::from("Test".to_owned()), + title: HorizontallyScrollableText::from("Test"), original_language: language(), status: "released".to_owned(), overview: "New movie blah blah blah".to_owned(), @@ -1966,7 +1979,7 @@ mod test { fn movie_history_item() -> MovieHistoryItem { MovieHistoryItem { - source_title: HorizontallyScrollableText::from("Test".to_owned()), + source_title: HorizontallyScrollableText::from("Test"), quality: quality_wrapper(), languages: vec![language()], date: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()), @@ -1982,9 +1995,7 @@ mod test { movie_id: Number::from(1), size: Number::from(3543348019u64), sizeleft: Number::from(1771674009u64), - output_path: Some(HorizontallyScrollableText::from( - "/nfs/movies/Test".to_owned(), - )), + output_path: Some(HorizontallyScrollableText::from("/nfs/movies/Test")), indexer: "kickass torrents".to_owned(), download_client: "transmission".to_owned(), } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4365df3..d4f0873 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -22,7 +22,7 @@ use crate::ui::utils::{ layout_button_paragraph_borderless, layout_paragraph_borderless, logo_block, show_cursor, style_block_highlight, style_default, style_default_bold, style_failure, style_help, style_highlight, style_primary, style_secondary, style_system_function, title_block, - title_block_centered, vertical_chunks, vertical_chunks_with_margin, + title_block_centered, vertical_chunks_with_margin, }; mod radarr_ui; @@ -209,7 +209,7 @@ pub fn draw_large_popup_over( draw_popup_over(f, app, area, background_fn, popup_fn, 75, 75); } -pub fn draw_large_popup_over_ui( +pub fn draw_large_popup_over_background_fn_with_ui( f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect, @@ -296,6 +296,14 @@ pub struct TableProps<'a, T> { pub help: Option<&'static str>, } +pub struct ListProps<'a, T> { + pub content: &'a mut StatefulList, + pub title: &'static str, + pub is_loading: bool, + pub is_popup: bool, + pub help: Option<&'static str>, +} + fn draw_table<'a, B, T, F>( f: &mut Frame<'_, B>, content_area: Rect, @@ -315,23 +323,7 @@ fn draw_table<'a, B, T, F>( help, } = table_props; - let content_area = if let Some(help_string) = help { - let chunks = vertical_chunks( - vec![Constraint::Min(0), Constraint::Length(2)], - content_area, - ); - let mut help_text = Text::from(format!(" {}", help_string)); - help_text.patch_style(style_help()); - let help_paragraph = Paragraph::new(help_text) - .block(layout_block_top_border()) - .alignment(Alignment::Left); - - f.render_widget(help_paragraph, chunks[1]); - - chunks[0] - } else { - content_area - }; + let content_area = draw_help(f, content_area, help); if !content.items.is_empty() { let rows = content.items.iter().map(row_mapper); @@ -587,20 +579,54 @@ pub fn draw_selectable_list<'a, B: Backend, T>( pub fn draw_list_box<'a, B: Backend, T>( f: &mut Frame<'_, B>, area: Rect, - content: &'a mut StatefulList, - title: &str, item_mapper: impl Fn(&T) -> ListItem<'a>, - is_loading: bool, + list_props: ListProps<'a, T>, ) { - let block = title_block(title); + let ListProps { + content, + title, + is_loading, + is_popup, + help, + } = list_props; + + let (content_area, block) = if is_popup { + f.render_widget(title_block(title), area); + (draw_help(f, area, help), borderless_block()) + } else { + (area, title_block(title)) + }; if !content.items.is_empty() { let items: Vec> = content.items.iter().map(item_mapper).collect(); - let list = List::new(items).block(title_block(title)); + let mut list = List::new(items).block(block); - f.render_stateful_widget(list, area, &mut content.state); + if is_popup { + list = list.highlight_style(style_highlight()); + } + + f.render_stateful_widget(list, content_area, &mut content.state); } else { - loading(f, block, area, is_loading); + loading(f, block, content_area, is_loading); + } +} + +fn draw_help(f: &mut Frame<'_, B>, area: Rect, help: Option<&str>) -> Rect { + if let Some(help_string) = help { + let chunks = + vertical_chunks_with_margin(vec![Constraint::Min(0), Constraint::Length(2)], area, 1); + + let mut help_test = Text::from(format!(" {}", help_string)); + help_test.patch_style(style_help()); + let help_paragraph = Paragraph::new(help_test) + .block(layout_block_top_border()) + .alignment(Alignment::Left); + + f.render_widget(help_paragraph, chunks[1]); + + chunks[0] + } else { + area } } diff --git a/src/ui/radarr_ui/edit_collection_ui.rs b/src/ui/radarr_ui/edit_collection_ui.rs index 24c26eb..6051b0c 100644 --- a/src/ui/radarr_ui/edit_collection_ui.rs +++ b/src/ui/radarr_ui/edit_collection_ui.rs @@ -15,7 +15,8 @@ use crate::ui::utils::{ }; use crate::ui::{ draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, - draw_large_popup_over_ui, draw_medium_popup_over, draw_popup, draw_text_box_with_label, DrawUi, + draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup, + draw_text_box_with_label, DrawUi, }; pub(super) struct EditCollectionUi {} @@ -62,7 +63,7 @@ impl DrawUi for EditCollectionUi { draw_edit_collection_prompt, ), _ if COLLECTION_DETAILS_BLOCKS.contains(&context) => { - draw_large_popup_over_ui::( + draw_large_popup_over_background_fn_with_ui::( f, app, content_rect, diff --git a/src/ui/radarr_ui/edit_movie_ui.rs b/src/ui/radarr_ui/edit_movie_ui.rs index 2db9424..90ea2ad 100644 --- a/src/ui/radarr_ui/edit_movie_ui.rs +++ b/src/ui/radarr_ui/edit_movie_ui.rs @@ -15,7 +15,8 @@ use crate::ui::utils::{ }; use crate::ui::{ draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, - draw_large_popup_over_ui, draw_medium_popup_over, draw_popup, draw_text_box_with_label, DrawUi, + draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup, + draw_text_box_with_label, DrawUi, }; pub(super) struct EditMovieUi {} @@ -58,7 +59,12 @@ impl DrawUi for EditMovieUi { draw_medium_popup_over(f, app, content_rect, draw_library, draw_edit_movie_prompt); } _ if MOVIE_DETAILS_BLOCKS.contains(&context) => { - draw_large_popup_over_ui::(f, app, content_rect, draw_library); + draw_large_popup_over_background_fn_with_ui::( + f, + app, + content_rect, + draw_library, + ); draw_popup(f, app, draw_edit_movie_prompt, 60, 60); } _ => (), diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 86b0d11..83f5868 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -1,5 +1,4 @@ use std::iter; -use std::ops::Sub; use chrono::{Duration, Utc}; use tui::backend::Backend; @@ -13,6 +12,7 @@ use tui::Frame; use crate::app::radarr::{ ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_MOVIE_BLOCKS, FILTER_BLOCKS, MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, + SYSTEM_DETAILS_BLOCKS, }; use crate::app::App; use crate::logos::RADARR_LOGO; @@ -21,6 +21,7 @@ use crate::models::Route; use crate::ui::draw_selectable_list; use crate::ui::draw_tabs; use crate::ui::loading; +use crate::ui::radarr_ui::system_details_ui::SystemDetailsUi; use crate::ui::radarr_ui::system_ui::SystemUi; use crate::ui::radarr_ui::{ add_movie_ui::AddMoviesUi, collection_details_ui::CollectionDetailsUi, @@ -45,7 +46,9 @@ mod edit_collection_ui; mod edit_movie_ui; mod library_ui; mod movie_details_ui; +mod radarr_ui_utils; mod root_folders_ui; +mod system_details_ui; mod system_ui; pub(super) struct RadarrUi {} @@ -73,6 +76,9 @@ impl DrawUi for RadarrUi { | ActiveRadarrBlock::AddRootFolderPrompt | ActiveRadarrBlock::DeleteRootFolderPrompt => RootFoldersUi::draw(f, app, content_rect), ActiveRadarrBlock::System => SystemUi::draw(f, app, content_rect), + _ if SYSTEM_DETAILS_BLOCKS.contains(&active_radarr_block) => { + SystemDetailsUi::draw(f, app, content_rect) + } _ if MOVIE_DETAILS_BLOCKS.contains(&active_radarr_block) => { MovieDetailsUi::draw(f, app, content_rect) } @@ -143,15 +149,13 @@ fn draw_stats_context(f: &mut Frame<'_, B>, app: &App<'_>, area: Rec .block(borderless_block()) .style(style_bold()); - let uptime = Utc::now().sub(start_time.to_owned()); + let uptime = Utc::now() - start_time.to_owned(); let days = uptime.num_days(); - let day_difference = uptime.sub(Duration::days(days)); + let day_difference = uptime - Duration::days(days); let hours = day_difference.num_hours(); - let hour_difference = day_difference.sub(Duration::hours(hours)); + let hour_difference = day_difference - Duration::hours(hours); let minutes = hour_difference.num_minutes(); - let seconds = hour_difference - .sub(Duration::minutes(minutes)) - .num_seconds(); + let seconds = (hour_difference - Duration::minutes(minutes)).num_seconds(); let uptime_paragraph = Paragraph::new(Text::from(format!( "Uptime: {}d {:0width$}:{:0width$}:{:0width$}", diff --git a/src/ui/radarr_ui/radarr_ui_utils.rs b/src/ui/radarr_ui/radarr_ui_utils.rs new file mode 100644 index 0000000..e3cd4c9 --- /dev/null +++ b/src/ui/radarr_ui/radarr_ui_utils.rs @@ -0,0 +1,44 @@ +use crate::ui::utils::{style_default, style_failure, style_secondary}; +use tui::style::{Color, Modifier, Style}; + +#[cfg(test)] +#[path = "radarr_ui_utils_tests.rs"] +mod radarr_ui_utils_tests; + +pub(super) fn determine_log_style_by_level(level: &str) -> Style { + match level.to_lowercase().as_str() { + "trace" => Style::default().fg(Color::Gray), + "debug" => Style::default().fg(Color::Blue), + "info" => style_default(), + "warn" => style_secondary(), + "error" => style_failure(), + "fatal" => style_failure().add_modifier(Modifier::BOLD), + _ => style_default(), + } +} + +pub(super) fn convert_to_minutes_hours_days(time: i64) -> String { + if time < 60 { + if time == 0 { + "now".to_owned() + } else if time == 1 { + format!("{} minute", time) + } else { + format!("{} minutes", time) + } + } else if time / 60 < 24 { + let hours = time / 60; + if hours == 1 { + format!("{} hour", hours) + } else { + format!("{} hours", hours) + } + } else { + let days = time / (60 * 24); + if days == 1 { + format!("{} day", days) + } else { + format!("{} days", days) + } + } +} diff --git a/src/ui/radarr_ui/system_ui_tests.rs b/src/ui/radarr_ui/radarr_ui_utils_tests.rs similarity index 100% rename from src/ui/radarr_ui/system_ui_tests.rs rename to src/ui/radarr_ui/radarr_ui_utils_tests.rs diff --git a/src/ui/radarr_ui/system_details_ui.rs b/src/ui/radarr_ui/system_details_ui.rs new file mode 100644 index 0000000..4f08bda --- /dev/null +++ b/src/ui/radarr_ui/system_details_ui.rs @@ -0,0 +1,123 @@ +use crate::app::radarr::ActiveRadarrBlock; +use crate::app::App; +use crate::models::Route; +use crate::ui::radarr_ui::radarr_ui_utils::determine_log_style_by_level; +use crate::ui::radarr_ui::system_ui::{ + draw_queued_events, draw_system_ui_layout, extract_task_props, TASK_TABLE_CONSTRAINTS, + TASK_TABLE_HEADERS, +}; +use crate::ui::utils::{style_primary, title_block}; +use crate::ui::{ + draw_large_popup_over, draw_list_box, draw_medium_popup_over, draw_prompt_box, + draw_prompt_popup_over, draw_table, DrawUi, ListProps, TableProps, +}; +use tui::backend::Backend; +use tui::layout::Rect; +use tui::text::{Span, Text}; +use tui::widgets::{Cell, ListItem, Row}; +use tui::Frame; + +pub(super) struct SystemDetailsUi {} + +impl DrawUi for SystemDetailsUi { + fn draw(f: &mut Frame<'_, B>, app: &mut App<'_>, content_rect: Rect) { + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + match active_radarr_block { + ActiveRadarrBlock::SystemLogs => { + draw_large_popup_over(f, app, content_rect, draw_system_ui_layout, draw_logs_popup) + } + ActiveRadarrBlock::SystemTasks | ActiveRadarrBlock::SystemTaskStartConfirmPrompt => { + draw_large_popup_over( + f, + app, + content_rect, + draw_system_ui_layout, + draw_tasks_popup, + ) + } + ActiveRadarrBlock::SystemQueue => draw_medium_popup_over( + f, + app, + content_rect, + draw_system_ui_layout, + draw_queued_events, + ), + _ => (), + } + } + } +} + +fn draw_logs_popup(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { + draw_list_box( + f, + area, + |log| { + let log_line = log.to_string(); + let level = log.text.split('|').collect::>()[1]; + let style = determine_log_style_by_level(level); + + ListItem::new(Text::from(Span::raw(log_line))).style(style) + }, + ListProps { + content: &mut app.data.radarr_data.log_details, + title: "Log Details", + is_loading: app.is_loading, + is_popup: true, + help: Some(" close | <↑↓←→> scroll"), + }, + ); +} + +fn draw_tasks_popup(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { + let tasks_popup_table = |f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect| { + draw_table( + f, + area, + title_block("Tasks"), + TableProps { + content: &mut app.data.radarr_data.tasks, + table_headers: TASK_TABLE_HEADERS.to_vec(), + constraints: TASK_TABLE_CONSTRAINTS.to_vec(), + help: None, + }, + |task| { + 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.last_duration), + Cell::from(task_props.next_execution), + ]) + .style(style_primary()) + }, + app.is_loading, + true, + ) + }; + + if matches!( + app.get_current_route(), + Route::Radarr(ActiveRadarrBlock::SystemTaskStartConfirmPrompt, _) + ) { + draw_prompt_popup_over(f, app, area, tasks_popup_table, draw_start_task_prompt) + } else { + tasks_popup_table(f, app, area); + } +} + +fn draw_start_task_prompt(f: &mut Frame<'_, B>, app: &mut App<'_>, prompt_area: Rect) { + draw_prompt_box( + f, + prompt_area, + "Start Task", + format!( + "Do you want to manually start this task: {}?", + app.data.radarr_data.tasks.current_selection().name + ) + .as_str(), + app.data.radarr_data.prompt_confirm, + ); +} diff --git a/src/ui/radarr_ui/system_ui.rs b/src/ui/radarr_ui/system_ui.rs index 9caf88c..64eccab 100644 --- a/src/ui/radarr_ui/system_ui.rs +++ b/src/ui/radarr_ui/system_ui.rs @@ -1,31 +1,45 @@ -use crate::ui::utils::{layout_block_top_border, style_help, style_primary, style_secondary}; -use crate::ui::{draw_table, TableProps}; +use crate::models::radarr_models::Task; +use crate::ui::radarr_ui::radarr_ui_utils::{ + convert_to_minutes_hours_days, determine_log_style_by_level, +}; +use crate::ui::utils::{layout_block_top_border, style_help, style_primary}; +use crate::ui::{draw_table, ListProps, TableProps}; use crate::{ app::{radarr::ActiveRadarrBlock, App}, models::Route, ui::{ draw_list_box, - utils::{horizontal_chunks, style_default, style_failure, title_block, vertical_chunks}, + utils::{horizontal_chunks, title_block, vertical_chunks}, DrawUi, }, }; use chrono::Utc; use std::ops::Sub; use tui::layout::Alignment; -use tui::style::Modifier; use tui::text::{Span, Text}; use tui::widgets::{Cell, Paragraph, Row}; use tui::{ backend::Backend, layout::{Constraint, Rect}, - style::{Color, Style}, widgets::ListItem, Frame, }; -#[cfg(test)] -#[path = "system_ui_tests.rs"] -mod system_ui_tests; +pub(super) const TASK_TABLE_HEADERS: [&str; 5] = [ + "Name", + "Interval", + "Last Execution", + "Last Duration", + "Next Execution", +]; + +pub(super) const TASK_TABLE_CONSTRAINTS: [Constraint; 5] = [ + Constraint::Percentage(30), + Constraint::Percentage(12), + Constraint::Percentage(18), + Constraint::Percentage(18), + Constraint::Percentage(22), +]; pub(super) struct SystemUi {} @@ -40,7 +54,11 @@ impl DrawUi for SystemUi { } } -fn draw_system_ui_layout(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { +pub(super) fn draw_system_ui_layout( + f: &mut Frame<'_, B>, + app: &mut App<'_>, + area: Rect, +) { let vertical_chunks = vertical_chunks( vec![ Constraint::Ratio(1, 2), @@ -56,54 +74,31 @@ fn draw_system_ui_layout(f: &mut Frame<'_, B>, app: &mut App<'_>, ar ); draw_tasks(f, app, horizontal_chunks[0]); - draw_events(f, app, horizontal_chunks[1]); + draw_queued_events(f, app, horizontal_chunks[1]); draw_logs(f, app, vertical_chunks[1]); draw_help(f, app, vertical_chunks[2]); } fn draw_tasks(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { - let block = title_block("Tasks"); draw_table( f, area, - block, + title_block("Tasks"), TableProps { content: &mut app.data.radarr_data.tasks, - table_headers: vec![ - "Name", - "Interval", - "Last Execution", - "Last Duration", - "Next Execution", - ], - constraints: vec![ - Constraint::Percentage(30), - Constraint::Percentage(12), - Constraint::Percentage(18), - Constraint::Percentage(18), - Constraint::Percentage(22), - ], + table_headers: TASK_TABLE_HEADERS.to_vec(), + constraints: TASK_TABLE_CONSTRAINTS.to_vec(), help: None, }, |task| { - let interval = convert_to_minutes_hours_days(*task.interval.as_i64().as_ref().unwrap()); - let last_duration = &task.last_duration[..8]; - let next_execution = - convert_to_minutes_hours_days(task.next_execution.sub(Utc::now()).num_minutes()); - let last_execution = - convert_to_minutes_hours_days(Utc::now().sub(task.last_execution).num_minutes()); - let last_execution_string = if last_execution != "now" { - format!("{} ago", last_execution) - } else { - last_execution - }; + let task_props = extract_task_props(task); Row::new(vec![ - Cell::from(task.name.clone()), - Cell::from(interval), - Cell::from(last_execution_string), - Cell::from(last_duration.to_owned()), - Cell::from(next_execution), + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.last_duration), + Cell::from(task_props.next_execution), ]) .style(style_primary()) }, @@ -112,14 +107,13 @@ fn draw_tasks(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { ); } -fn draw_events(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { - let block = title_block("Events"); +pub(super) fn draw_queued_events(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { draw_table( f, area, - block, + title_block("Queued Events"), TableProps { - content: &mut app.data.radarr_data.events, + content: &mut app.data.radarr_data.queued_events, table_headers: vec!["Trigger", "Status", "Name", "Queued", "Started", "Duration"], constraints: vec![ Constraint::Percentage(13), @@ -151,7 +145,11 @@ fn draw_events(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) String::new() }; - let duration = &event.duration[..8]; + let duration = if event.duration.is_some() { + &event.duration.as_ref().unwrap()[..8] + } else { + "" + }; Row::new(vec![ Cell::from(event.trigger.clone()), @@ -172,31 +170,20 @@ fn draw_logs(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { draw_list_box( f, area, - &mut app.data.radarr_data.logs, - "Logs", |log| { - let log_line = if log.exception.is_some() { - Text::from(Span::raw(format!( - "{}|{}|{}|{}|{}", - log.time, - log.level.as_ref().unwrap().to_uppercase(), - log.logger.as_ref().unwrap(), - log.exception_type.as_ref().unwrap(), - log.exception.as_ref().unwrap() - ))) - } else { - Text::from(Span::raw(format!( - "{}|{}|{}|{}", - log.time, - log.level.as_ref().unwrap().to_uppercase(), - log.logger.as_ref().unwrap(), - log.message.as_ref().unwrap() - ))) - }; + let log_line = log.to_string(); + let level = log_line.split('|').collect::>()[1]; + let style = determine_log_style_by_level(level); - ListItem::new(log_line).style(determine_log_style_by_level(log.level.as_ref().unwrap())) + ListItem::new(Text::from(Span::raw(log_line))).style(style) + }, + ListProps { + content: &mut app.data.radarr_data.logs, + title: "Logs", + is_loading: app.is_loading, + is_popup: false, + help: None, }, - app.is_loading, ); } @@ -218,40 +205,32 @@ fn draw_help(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { f.render_widget(help_paragraph, area); } -fn determine_log_style_by_level(level: &str) -> Style { - match level.to_lowercase().as_str() { - "trace" => Style::default().fg(Color::Gray), - "debug" => Style::default().fg(Color::Blue), - "info" => style_default(), - "warn" => style_secondary(), - "error" => style_failure(), - "fatal" => style_failure().add_modifier(Modifier::BOLD), - _ => style_default(), - } +pub(super) struct TaskProps { + pub(super) name: String, + pub(super) interval: String, + pub(super) last_execution: String, + pub(super) last_duration: String, + pub(super) next_execution: String, } -fn convert_to_minutes_hours_days(time: i64) -> String { - if time < 60 { - if time == 0 { - "now".to_owned() - } else if time == 1 { - format!("{} minute", time) - } else { - format!("{} minutes", time) - } - } else if time / 60 < 24 { - let hours = time / 60; - if hours == 1 { - format!("{} hour", hours) - } else { - format!("{} hours", hours) - } +pub(super) fn extract_task_props(task: &Task) -> TaskProps { + let interval = convert_to_minutes_hours_days(*task.interval.as_i64().as_ref().unwrap()); + let last_duration = &task.last_duration[..8]; + 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!("{} ago", last_execution) } else { - let days = time / (60 * 24); - if days == 1 { - format!("{} day", days) - } else { - format!("{} days", days) - } + last_execution + }; + + TaskProps { + name: task.name.clone(), + interval, + last_execution: last_execution_string, + last_duration: last_duration.to_owned(), + next_execution, } }