diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 3e939ef..f15a20a 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -62,6 +62,11 @@ impl App<'_> { .dispatch_network_event(RadarrEvent::GetDownloads(500).into()) .await; } + ActiveRadarrBlock::History => { + self + .dispatch_network_event(RadarrEvent::GetHistory(500).into()) + .await; + } ActiveRadarrBlock::Indexers => { self .dispatch_network_event(RadarrEvent::GetTags.into()) diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index d5fbca9..2306ce8 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -3,8 +3,8 @@ mod tests { use crate::app::App; use crate::app::context_clues::{ BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, - ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::radarr_context_clues::{ @@ -459,9 +459,10 @@ mod tests { #[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)] #[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)] #[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)] - #[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)] - #[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)] - #[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)] + #[case(4, ActiveRadarrBlock::History, &HISTORY_CONTEXT_CLUES)] + #[case(5, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)] + #[case(6, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)] + #[case(7, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)] fn test_radarr_context_clue_provider_radarr_blocks_context_clues( #[case] index: usize, #[case] active_radarr_block: ActiveRadarrBlock, diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 0783f76..e42b347 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -147,6 +147,23 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_history_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::History) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetHistory(500).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_dispatch_by_root_folders_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs index 4e613c9..15dd974 100644 --- a/src/cli/radarr/list_command_handler.rs +++ b/src/cli/radarr/list_command_handler.rs @@ -29,6 +29,11 @@ pub enum RadarrListCommand { }, #[command(about = "List disk space details for all provisioned root folders in Radarr")] DiskSpace, + #[command(about = "Fetch all Radarr history events")] + History { + #[arg(long, help = "How many history events to fetch", default_value_t = 500)] + events: u64, + }, #[command(about = "List all Radarr indexers")] Indexers, #[command(about = "Fetch Radarr logs")] @@ -121,6 +126,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + RadarrListCommand::History { events: items } => { + let resp = self + .network + .handle_network_event(RadarrEvent::GetHistory(items).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } RadarrListCommand::Indexers => { let resp = self .network diff --git a/src/cli/radarr/list_command_handler_tests.rs b/src/cli/radarr/list_command_handler_tests.rs index ba5f810..207d484 100644 --- a/src/cli/radarr/list_command_handler_tests.rs +++ b/src/cli/radarr/list_command_handler_tests.rs @@ -111,6 +111,29 @@ mod tests { assert_eq!(refresh_command, expected_args); } + #[test] + fn test_list_history_events_flag_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "radarr", "list", "history", "--events"]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_list_history_default_values() { + let expected_args = RadarrListCommand::History { events: 500 }; + let result = Cli::try_parse_from(["managarr", "radarr", "list", "history"]); + + assert_ok!(&result); + + let Some(Command::Radarr(RadarrCommand::List(history_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(history_command, expected_args); + } + #[test] fn test_list_logs_default_values() { let expected_args = RadarrListCommand::Logs { @@ -233,6 +256,32 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_handle_list_history_command() { + let expected_events = 1000; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::GetHistory(expected_events).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_history_command = RadarrListCommand::History { events: 1000 }; + + let result = + RadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_list_logs_command() { let expected_events = 1000; diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index f13a587..97ada3b 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -64,6 +64,15 @@ pub enum RadarrCommand { Refresh(RadarrRefreshCommand), #[command(about = "Clear the blocklist")] ClearBlocklist, + #[command(about = "Mark the Radarr history item with the given ID as 'failed'")] + MarkHistoryItemAsFailed { + #[arg( + long, + help = "The Radarr ID of the history item you wish to mark as 'failed'", + required = true + )] + history_item_id: i64, + }, #[command(about = "Manually download the given release for the specified movie ID")] DownloadRelease { #[arg(long, help = "The GUID of the release to download", required = true)] @@ -208,6 +217,15 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' .await?; serde_json::to_string_pretty(&resp)? } + RadarrCommand::MarkHistoryItemAsFailed { history_item_id } => { + let _ = self + .network + .handle_network_event(RadarrEvent::MarkHistoryItemAsFailed(history_item_id).into()) + .await?; + serde_json::to_string_pretty( + &serde_json::json!({"message": "Radarr history item marked as 'failed'"}), + )? + } RadarrCommand::DownloadRelease { guid, indexer_id, diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index af2f15f..25825c1 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -31,6 +31,31 @@ mod tests { assert_ok!(&result); } + #[test] + fn test_mark_history_item_as_failed_requires_history_item_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "radarr", "mark-history-item-as-failed"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_mark_history_item_as_failed_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "mark-history-item-as-failed", + "--history-item-id", + "1", + ]); + + assert_ok!(&result); + } + #[test] fn test_download_release_requires_movie_id() { let result = Cli::command().try_get_matches_from([ @@ -327,6 +352,36 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_mark_history_item_as_failed_command() { + let expected_history_item_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let mark_history_item_as_failed_command = + RadarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 }; + + let result = RadarrCliHandler::with( + &app_arc, + mark_history_item_as_failed_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_download_release_command() { let expected_release_download_body = RadarrReleaseDownloadBody { diff --git a/src/handlers/handler_test_utils.rs b/src/handlers/handler_test_utils.rs index f3348c4..268e5ce 100644 --- a/src/handlers/handler_test_utils.rs +++ b/src/handlers/handler_test_utils.rs @@ -356,6 +356,9 @@ mod test_utils { .radarr_data .movies .set_items(vec![$crate::models::radarr_models::Movie::default()]); + app.data.radarr_data.history.set_items(vec![ + $crate::models::radarr_models::RadarrHistoryItem::default(), + ]); app .data .radarr_data diff --git a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs index af27925..bc53e17 100644 --- a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs @@ -91,9 +91,9 @@ mod tests { assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::History.into() ); - assert_navigation_pushed!(app, ActiveRadarrBlock::RootFolders.into()); + assert_navigation_pushed!(app, ActiveRadarrBlock::History.into()); } #[rstest] diff --git a/src/handlers/radarr_handlers/history/history_handler_tests.rs b/src/handlers/radarr_handlers/history/history_handler_tests.rs new file mode 100644 index 0000000..4bd6e51 --- /dev/null +++ b/src/handlers/radarr_handlers/history/history_handler_tests.rs @@ -0,0 +1,438 @@ +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use chrono::DateTime; + 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::radarr_handlers::history::{HistoryHandler, history_sorting_options}; + use crate::models::radarr_models::{RadarrHistoryEventType, RadarrHistoryItem}; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS}; + use crate::models::servarr_models::{Language, Quality, QualityWrapper}; + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_history_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = is_ready; + app.data.radarr_data.main_tabs.set_index(4); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveRadarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.radarr_data.main_tabs.get_active_route(), + ActiveRadarrBlock::Blocklist.into() + ); + assert_navigation_pushed!(app, ActiveRadarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_history_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = is_ready; + app.data.radarr_data.main_tabs.set_index(4); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveRadarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.radarr_data.main_tabs.get_active_route(), + ActiveRadarrBlock::RootFolders.into() + ); + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_history_submit() { + let mut app = App::test_default(); + app.data.radarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::History, None).handle(); + + assert_navigation_pushed!(app, ActiveRadarrBlock::HistoryItemDetails.into()); + } + + #[test] + fn test_history_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.radarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_esc_history_item_details() { + let mut app = App::test_default(); + app + .data + .radarr_data + .history + .set_items(vec![RadarrHistoryItem::default()]); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.push_navigation_stack(ActiveRadarrBlock::HistoryItemDetails.into()); + + HistoryHandler::new( + ESC_KEY, + &mut app, + ActiveRadarrBlock::HistoryItemDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveRadarrBlock::History.into()); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.data.radarr_data = create_test_radarr_data(); + app + .data + .radarr_data + .history + .set_items(vec![RadarrHistoryItem::default()]); + + HistoryHandler::new(ESC_KEY, &mut app, ActiveRadarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use super::*; + use crate::assert_navigation_pushed; + + #[test] + fn test_refresh_history_key() { + let mut app = App::test_default(); + app.data.radarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveRadarrBlock::History, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveRadarrBlock::History.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_history_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.radarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveRadarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into()); + assert!(!app.should_refresh); + } + } + + #[test] + fn test_history_sorting_options_source_title() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[0].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_history_sorting_options_event_type() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| { + a.event_type + .to_string() + .to_lowercase() + .cmp(&b.event_type.to_string().to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[1].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Event Type"); + } + + #[test] + fn test_history_sorting_options_language() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| { + let default_language = Language { + id: 1, + name: "_".to_owned(), + }; + let language_a = a.languages.first().unwrap_or(&default_language); + let language_b = b.languages.first().unwrap_or(&default_language); + + language_a.cmp(language_b) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[2].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Language"); + } + + #[test] + fn test_history_sorting_options_quality() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[3].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_history_sorting_options_date() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[4].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + #[test] + fn test_history_handler_accepts() { + ActiveRadarrBlock::iter().for_each(|active_radarr_block| { + if HISTORY_BLOCKS.contains(&active_radarr_block) { + assert!(HistoryHandler::accepts(active_radarr_block)); + } else { + assert!(!HistoryHandler::accepts(active_radarr_block)); + } + }) + } + + #[rstest] + fn test_history_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 = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_history_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = true; + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_not_ready_when_history_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = false; + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_ready_when_not_loading_and_history_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = false; + app + .data + .radarr_data + .history + .set_items(vec![RadarrHistoryItem::default()]); + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::History, + None, + ); + + assert!(handler.is_ready()); + } + + fn history_vec() -> Vec { + vec![ + RadarrHistoryItem { + id: 3, + source_title: "test 1".into(), + movie_id: 1, + event_type: RadarrHistoryEventType::Grabbed, + languages: vec![Language { + id: 1, + name: "telgu".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + ..RadarrHistoryItem::default() + }, + RadarrHistoryItem { + id: 2, + source_title: "test 2".into(), + movie_id: 2, + event_type: RadarrHistoryEventType::DownloadFolderImported, + languages: vec![Language { + id: 3, + name: "chinese".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "SD - 720p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + ..RadarrHistoryItem::default() + }, + RadarrHistoryItem { + id: 1, + source_title: "test 3".into(), + movie_id: 3, + event_type: RadarrHistoryEventType::MovieFileDeleted, + languages: vec![Language { + id: 1, + name: "english".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + ..RadarrHistoryItem::default() + }, + ] + } +} diff --git a/src/handlers/radarr_handlers/history/mod.rs b/src/handlers/radarr_handlers/history/mod.rs new file mode 100644 index 0000000..a424b2e --- /dev/null +++ b/src/handlers/radarr_handlers/history/mod.rs @@ -0,0 +1,179 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors}; +use crate::matches_key; +use crate::models::Route; +use crate::models::radarr_models::RadarrHistoryItem; +use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS}; +use crate::models::servarr_models::Language; +use crate::models::stateful_table::SortOption; + +#[cfg(test)] +#[path = "history_handler_tests.rs"] +mod history_handler_tests; + +pub(super) struct HistoryHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_radarr_block: ActiveRadarrBlock, + _context: Option, +} + +impl HistoryHandler<'_, '_> {} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for HistoryHandler<'a, 'b> { + fn handle(&mut self) { + let history_table_handling_config = TableHandlingConfig::new(ActiveRadarrBlock::History.into()) + .sorting_block(ActiveRadarrBlock::HistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .searching_block(ActiveRadarrBlock::SearchHistory.into()) + .search_error_block(ActiveRadarrBlock::SearchHistoryError.into()) + .search_field_fn(|history| &history.source_title.text) + .filtering_block(ActiveRadarrBlock::FilterHistory.into()) + .filter_error_block(ActiveRadarrBlock::FilterHistoryError.into()) + .filter_field_fn(|history| &history.source_title.text); + + if !handle_table( + self, + |app| &mut app.data.radarr_data.history, + history_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + HISTORY_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveRadarrBlock, + context: Option, + ) -> Self { + HistoryHandler { + key, + app, + active_radarr_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.radarr_data.history.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_radarr_block == ActiveRadarrBlock::History { + handle_change_tab_left_right_keys(self.app, self.key) + } + } + + fn handle_submit(&mut self) { + if self.active_radarr_block == ActiveRadarrBlock::History { + self + .app + .push_navigation_stack(ActiveRadarrBlock::HistoryItemDetails.into()); + } + } + + fn handle_esc(&mut self) { + if self.active_radarr_block == ActiveRadarrBlock::HistoryItemDetails { + self.app.pop_navigation_stack(); + } else { + handle_clear_errors(self.app); + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + if self.active_radarr_block == ActiveRadarrBlock::History { + match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ => (), + } + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +pub(in crate::handlers::radarr_handlers) fn history_sorting_options() +-> Vec> { + vec![ + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }), + }, + SortOption { + name: "Event Type", + cmp_fn: Some(|a, b| { + a.event_type + .to_string() + .to_lowercase() + .cmp(&b.event_type.to_string().to_lowercase()) + }), + }, + SortOption { + name: "Language", + cmp_fn: Some(|a, b| { + let default_language = Language { + id: 1, + name: "_".to_owned(), + }; + let language_a = a.languages.first().unwrap_or(&default_language); + let language_b = b.languages.first().unwrap_or(&default_language); + + language_a.cmp(language_b) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs index e25c24c..cc1885d 100644 --- a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs @@ -65,7 +65,7 @@ mod tests { fn test_indexers_tab_left(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(5); + app.data.radarr_data.main_tabs.set_index(6); IndexersHandler::new( DEFAULT_KEYBINDINGS.left.key, @@ -86,7 +86,7 @@ mod tests { fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(5); + app.data.radarr_data.main_tabs.set_index(6); IndexersHandler::new( DEFAULT_KEYBINDINGS.right.key, diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 282ce3b..0188e29 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -2,6 +2,7 @@ use crate::handlers::KeyEventHandler; use crate::handlers::radarr_handlers::blocklist::BlocklistHandler; use crate::handlers::radarr_handlers::collections::CollectionsHandler; use crate::handlers::radarr_handlers::downloads::DownloadsHandler; +use crate::handlers::radarr_handlers::history::HistoryHandler; use crate::handlers::radarr_handlers::indexers::IndexersHandler; use crate::handlers::radarr_handlers::library::LibraryHandler; use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler; @@ -13,6 +14,7 @@ use crate::{App, Key, matches_key}; mod blocklist; mod collections; mod downloads; +mod history; mod indexers; mod library; mod root_folders; @@ -51,6 +53,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b _ if DownloadsHandler::accepts(self.active_radarr_block) => { DownloadsHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle() } + _ if HistoryHandler::accepts(self.active_radarr_block) => { + HistoryHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle() + } _ if RootFoldersHandler::accepts(self.active_radarr_block) => { RootFoldersHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle() } diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index 0a3bc46..5333f69 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -16,10 +16,11 @@ mod tests { #[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)] #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)] #[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)] - #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)] - #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)] - #[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] - #[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] + #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::History)] + #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::RootFolders)] + #[case(5, ActiveRadarrBlock::History, ActiveRadarrBlock::Indexers)] + #[case(6, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] + #[case(7, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] fn test_radarr_handler_change_tab_left_right_keys( #[case] index: usize, #[case] left_block: ActiveRadarrBlock, @@ -51,10 +52,11 @@ mod tests { #[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)] #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)] #[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)] - #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)] - #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)] - #[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] - #[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] + #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::History)] + #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::RootFolders)] + #[case(5, ActiveRadarrBlock::History, ActiveRadarrBlock::Indexers)] + #[case(6, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] + #[case(7, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] fn test_radarr_handler_change_tab_left_right_keys_alt_navigation( #[case] index: usize, #[case] left_block: ActiveRadarrBlock, @@ -88,9 +90,10 @@ mod tests { #[case(1, ActiveRadarrBlock::Collections)] #[case(2, ActiveRadarrBlock::Downloads)] #[case(3, ActiveRadarrBlock::Blocklist)] - #[case(4, ActiveRadarrBlock::RootFolders)] - #[case(5, ActiveRadarrBlock::Indexers)] - #[case(6, ActiveRadarrBlock::System)] + #[case(4, ActiveRadarrBlock::History)] + #[case(5, ActiveRadarrBlock::RootFolders)] + #[case(6, ActiveRadarrBlock::Indexers)] + #[case(7, ActiveRadarrBlock::System)] fn test_radarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key( #[case] index: usize, #[case] block: ActiveRadarrBlock, @@ -281,6 +284,26 @@ mod tests { ); } + #[rstest] + fn test_delegates_history_blocks_to_history_handler( + #[values( + ActiveRadarrBlock::History, + ActiveRadarrBlock::HistoryItemDetails, + ActiveRadarrBlock::HistorySortPrompt, + ActiveRadarrBlock::FilterHistory, + ActiveRadarrBlock::FilterHistoryError, + ActiveRadarrBlock::SearchHistory, + ActiveRadarrBlock::SearchHistoryError + )] + active_radarr_block: ActiveRadarrBlock, + ) { + test_handler_delegation!( + RadarrHandler, + ActiveRadarrBlock::History, + active_radarr_block + ); + } + #[test] fn test_radarr_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { diff --git a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs index a032253..9c42cb2 100644 --- a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs @@ -130,7 +130,7 @@ mod tests { fn test_root_folders_tab_left(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(4); + app.data.radarr_data.main_tabs.set_index(5); RootFoldersHandler::new( DEFAULT_KEYBINDINGS.left.key, @@ -142,16 +142,16 @@ mod tests { assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - ActiveRadarrBlock::Blocklist.into() + ActiveRadarrBlock::History.into() ); - assert_navigation_pushed!(app, ActiveRadarrBlock::Blocklist.into()); + assert_navigation_pushed!(app, ActiveRadarrBlock::History.into()); } #[rstest] fn test_root_folders_tab_right(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(4); + app.data.radarr_data.main_tabs.set_index(5); RootFoldersHandler::new( DEFAULT_KEYBINDINGS.right.key, diff --git a/src/handlers/radarr_handlers/system/system_handler_tests.rs b/src/handlers/radarr_handlers/system/system_handler_tests.rs index cd4ab61..0c95818 100644 --- a/src/handlers/radarr_handlers/system/system_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_handler_tests.rs @@ -26,7 +26,7 @@ mod tests { fn test_system_tab_left(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(6); + app.data.radarr_data.main_tabs.set_index(7); SystemHandler::new( DEFAULT_KEYBINDINGS.left.key, @@ -47,7 +47,7 @@ mod tests { fn test_system_tab_right(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(6); + app.data.radarr_data.main_tabs.set_index(7); SystemHandler::new( DEFAULT_KEYBINDINGS.right.key, diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 826d513..baf1c62 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -408,6 +408,69 @@ pub struct SystemStatus { pub start_time: DateTime, } +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RadarrHistoryWrapper { + pub records: Vec, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RadarrHistoryData { + pub indexer: Option, + pub release_group: Option, + pub nzb_info_url: Option, + pub download_client: Option, + pub download_client_name: Option, + pub age: Option, + pub published_date: Option>, + pub message: Option, + pub reason: Option, + pub dropped_path: Option, + pub imported_path: Option, + pub source_path: Option, + pub path: Option, +} + +#[derive( + Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Display, EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum RadarrHistoryEventType { + #[default] + Unknown, + Grabbed, + #[display_style(name = "Download Folder Imported")] + DownloadFolderImported, + #[display_style(name = "Download Failed")] + DownloadFailed, + #[display_style(name = "Movie File Deleted")] + MovieFileDeleted, + #[display_style(name = "Movie Folder Imported")] + MovieFolderImported, + #[display_style(name = "Movie File Renamed")] + MovieFileRenamed, + #[display_style(name = "Download Ignored")] + DownloadIgnored, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RadarrHistoryItem { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub source_title: HorizontallyScrollableText, + #[serde(deserialize_with = "super::from_i64")] + pub movie_id: i64, + pub quality: QualityWrapper, + pub languages: Vec, + pub date: DateTime, + pub event_type: RadarrHistoryEventType, + #[serde(default)] + pub data: RadarrHistoryData, +} + #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct RadarrTask { @@ -461,6 +524,7 @@ serde_enum_from!( Credits(Vec), DiskSpaces(Vec), DownloadsResponse(DownloadsResponse), + HistoryWrapper(RadarrHistoryWrapper), HostConfig(HostConfig), Indexers(Vec), IndexerSettings(IndexerSettings), diff --git a/src/models/radarr_models_tests.rs b/src/models/radarr_models_tests.rs index 0820f39..2c84364 100644 --- a/src/models/radarr_models_tests.rs +++ b/src/models/radarr_models_tests.rs @@ -3,6 +3,9 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use serde_json::json; + use crate::models::radarr_models::{ + RadarrHistoryEventType, RadarrHistoryItem, RadarrHistoryWrapper, + }; use crate::models::{ Serdeable, radarr_models::{ @@ -61,6 +64,66 @@ mod tests { assert_str_eq!(MovieMonitor::None.to_display_str(), "None"); } + #[test] + fn test_radarr_history_event_type_display() { + assert_str_eq!(RadarrHistoryEventType::Unknown.to_string(), "unknown"); + assert_str_eq!(RadarrHistoryEventType::Grabbed.to_string(), "grabbed"); + assert_str_eq!( + RadarrHistoryEventType::DownloadFolderImported.to_string(), + "downloadFolderImported" + ); + assert_str_eq!( + RadarrHistoryEventType::DownloadFailed.to_string(), + "downloadFailed" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFileDeleted.to_string(), + "movieFileDeleted" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFolderImported.to_string(), + "movieFolderImported" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFileRenamed.to_string(), + "movieFileRenamed" + ); + assert_str_eq!( + RadarrHistoryEventType::DownloadIgnored.to_string(), + "downloadIgnored" + ); + } + + #[test] + fn test_radarr_history_event_type_to_display_str() { + assert_str_eq!(RadarrHistoryEventType::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(RadarrHistoryEventType::Grabbed.to_display_str(), "Grabbed"); + assert_str_eq!( + RadarrHistoryEventType::DownloadFolderImported.to_display_str(), + "Download Folder Imported" + ); + assert_str_eq!( + RadarrHistoryEventType::DownloadFailed.to_display_str(), + "Download Failed" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFileDeleted.to_display_str(), + "Movie File Deleted" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFolderImported.to_display_str(), + "Movie Folder Imported" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFileRenamed.to_display_str(), + "Movie File Renamed" + ); + assert_str_eq!( + RadarrHistoryEventType::DownloadIgnored.to_display_str(), + "Download Ignored" + ); + } + #[test] fn test_download_record_default_indexer_value() { let json = r#"{ @@ -235,6 +298,23 @@ mod tests { ); } + #[test] + fn test_radarr_serdeable_from_history_wrapper() { + let history_wrapper = RadarrHistoryWrapper { + records: vec![RadarrHistoryItem { + id: 1, + ..RadarrHistoryItem::default() + }], + }; + + let radarr_serdeable: RadarrSerdeable = history_wrapper.clone().into(); + + assert_eq!( + radarr_serdeable, + RadarrSerdeable::HistoryWrapper(history_wrapper) + ); + } + #[test] fn test_radarr_serdeable_from_log_response() { let log_response = LogResponse { diff --git a/src/models/servarr_data/radarr/modals.rs b/src/models/servarr_data/radarr/modals.rs index 922757d..9dd2c59 100644 --- a/src/models/servarr_data/radarr/modals.rs +++ b/src/models/servarr_data/radarr/modals.rs @@ -15,6 +15,7 @@ use crate::models::{HorizontallyScrollableText, ScrollableText}; mod modals_tests; #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct MovieDetailsModal { pub movie_details: ScrollableText, pub file_details: String, @@ -97,6 +98,7 @@ impl From<&RadarrData<'_>> for EditIndexerModal { } #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct EditMovieModal { pub minimum_availability_list: StatefulList, pub quality_profile_list: StatefulList, @@ -157,6 +159,7 @@ impl From<&RadarrData<'_>> for EditMovieModal { } #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct AddMovieModal { pub root_folder_list: StatefulList, pub monitor_list: StatefulList, @@ -186,6 +189,7 @@ impl From<&RadarrData<'_>> for AddMovieModal { } #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct EditCollectionModal { pub monitored: Option, pub minimum_availability_list: StatefulList, diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index cbea414..22fc4a1 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -1,5 +1,5 @@ use crate::app::context_clues::{ - BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::radarr::radarr_context_clues::{ @@ -8,7 +8,7 @@ use crate::app::radarr::radarr_context_clues::{ }; use crate::models::radarr_models::{ AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord, - IndexerSettings, Movie, RadarrTask, + IndexerSettings, Movie, RadarrHistoryItem, RadarrTask, }; use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}; use crate::models::servarr_data::radarr::modals::{ @@ -34,7 +34,8 @@ use { crate::network::radarr_network::radarr_network_test_utils::test_utils::{ add_movie_search_result, blocklist_item, cast_credit, collection, collection_movie, crew_credit, download_record, indexer, log_line, movie, movie_history_item, - quality_profile_map, tags_map, task, torrent_release, updates, usenet_release, + quality_profile_map, radarr_history_item, tags_map, task, torrent_release, updates, + usenet_release, }, crate::network::servarr_test_utils::diskspace, crate::network::servarr_test_utils::indexer_test_result, @@ -62,6 +63,7 @@ pub struct RadarrData<'a> { pub downloads: StatefulTable, pub indexers: StatefulTable, pub blocklist: StatefulTable, + pub history: StatefulTable, pub quality_profile_map: BiMap, pub tags_map: BiMap, pub collections: StatefulTable, @@ -135,6 +137,7 @@ impl<'a> Default for RadarrData<'a> { downloads: StatefulTable::default(), indexers: StatefulTable::default(), blocklist: StatefulTable::default(), + history: StatefulTable::default(), quality_profile_map: BiMap::default(), tags_map: BiMap::default(), collections: StatefulTable::default(), @@ -184,6 +187,12 @@ impl<'a> Default for RadarrData<'a> { contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES), config: None, }, + TabRoute { + title: "History".to_string(), + route: ActiveRadarrBlock::History.into(), + contextual_help: Some(&HISTORY_CONTEXT_CLUES), + config: None, + }, TabRoute { title: "Root Folders".to_string(), route: ActiveRadarrBlock::RootFolders.into(), @@ -387,6 +396,10 @@ impl RadarrData<'_> { radarr_data.downloads.set_items(vec![download_record()]); radarr_data.blocklist.set_items(vec![blocklist_item()]); radarr_data.blocklist.sorting(vec![sort_option!(id)]); + radarr_data.history.set_items(vec![radarr_history_item()]); + radarr_data.history.sorting(vec![sort_option!(id)]); + radarr_data.history.search = Some("Something".into()); + radarr_data.history.filter = Some("Something".into()); radarr_data.indexers.set_items(vec![indexer()]); radarr_data.indexers.sorting(vec![sort_option!(id)]); radarr_data.indexers.search = Some("Something".into()); @@ -420,6 +433,13 @@ pub enum ActiveRadarrBlock { BlocklistClearAllItemsPrompt, BlocklistItemDetails, BlocklistSortPrompt, + History, + HistoryItemDetails, + HistorySortPrompt, + FilterHistory, + FilterHistoryError, + SearchHistory, + SearchHistoryError, Collections, CollectionsSortPrompt, CollectionDetails, @@ -538,6 +558,15 @@ pub static BLOCKLIST_BLOCKS: [ActiveRadarrBlock; 5] = [ ActiveRadarrBlock::BlocklistClearAllItemsPrompt, ActiveRadarrBlock::BlocklistSortPrompt, ]; +pub static HISTORY_BLOCKS: [ActiveRadarrBlock; 7] = [ + ActiveRadarrBlock::History, + ActiveRadarrBlock::HistoryItemDetails, + ActiveRadarrBlock::HistorySortPrompt, + ActiveRadarrBlock::FilterHistory, + ActiveRadarrBlock::FilterHistoryError, + ActiveRadarrBlock::SearchHistory, + ActiveRadarrBlock::SearchHistoryError, +]; pub static ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::AddMovieSearchInput, ActiveRadarrBlock::AddMovieSearchResults, diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index d85a740..15b25f0 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -2,8 +2,8 @@ mod tests { mod radarr_data_tests { use crate::app::context_clues::{ - BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::radarr::radarr_context_clues::{ COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, @@ -105,41 +105,42 @@ mod tests { fn test_radarr_data_defaults() { let radarr_data = RadarrData::default(); - assert!(radarr_data.root_folders.items.is_empty()); + assert_is_empty!(radarr_data.root_folders.items); assert_eq!(radarr_data.disk_space_vec, Vec::new()); - assert!(radarr_data.version.is_empty()); + assert_is_empty!(radarr_data.version); assert_eq!(radarr_data.start_time, >::default()); - assert!(radarr_data.movies.is_empty()); + assert_is_empty!(radarr_data.movies); assert_eq!(radarr_data.selected_block, BlockSelectionState::default()); - assert!(radarr_data.downloads.items.is_empty()); - assert!(radarr_data.indexers.items.is_empty()); - assert!(radarr_data.blocklist.items.is_empty()); - assert!(radarr_data.quality_profile_map.is_empty()); - assert!(radarr_data.tags_map.is_empty()); - assert!(radarr_data.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.queued_events.items.is_empty()); - assert!(radarr_data.updates.get_text().is_empty()); - assert!(radarr_data.add_movie_search.is_none()); - assert!(radarr_data.add_movie_modal.is_none()); - assert!(radarr_data.add_searched_movies.is_none()); - assert!(radarr_data.edit_movie_modal.is_none()); - assert!(radarr_data.edit_collection_modal.is_none()); - assert!(radarr_data.edit_root_folder.is_none()); - assert!(radarr_data.edit_indexer_modal.is_none()); - assert!(radarr_data.indexer_settings.is_none()); - assert!(radarr_data.indexer_test_errors.is_none()); - assert!(radarr_data.indexer_test_all_results.is_none()); - assert!(radarr_data.movie_details_modal.is_none()); - assert!(radarr_data.prompt_confirm_action.is_none()); + assert_is_empty!(radarr_data.downloads.items); + assert_is_empty!(radarr_data.indexers.items); + assert_is_empty!(radarr_data.blocklist.items); + assert_is_empty!(radarr_data.history.items); + assert_is_empty!(radarr_data.quality_profile_map); + assert_is_empty!(radarr_data.tags_map); + assert_is_empty!(radarr_data.collections.items); + assert_is_empty!(radarr_data.collection_movies.items); + assert_is_empty!(radarr_data.logs.items); + assert_is_empty!(radarr_data.log_details.items); + assert_is_empty!(radarr_data.tasks.items); + assert_is_empty!(radarr_data.queued_events.items); + assert_is_empty!(radarr_data.updates.get_text()); + assert_none!(&radarr_data.add_movie_search); + assert_none!(&radarr_data.add_movie_modal); + assert_none!(&radarr_data.add_searched_movies); + assert_none!(&radarr_data.edit_movie_modal); + assert_none!(&radarr_data.edit_collection_modal); + assert_none!(&radarr_data.edit_root_folder); + assert_none!(&radarr_data.edit_indexer_modal); + assert_none!(&radarr_data.indexer_settings); + assert_none!(&radarr_data.indexer_test_errors); + assert_none!(&radarr_data.indexer_test_all_results); + assert_none!(&radarr_data.movie_details_modal); + assert_none!(&radarr_data.prompt_confirm_action); assert!(!radarr_data.prompt_confirm); assert!(!radarr_data.delete_movie_files); assert!(!radarr_data.add_list_exclusion); - assert_eq!(radarr_data.main_tabs.tabs.len(), 7); + assert_eq!(radarr_data.main_tabs.tabs.len(), 8); assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -189,42 +190,54 @@ mod tests { ); assert_eq!(radarr_data.main_tabs.tabs[3].config, None); - assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Root Folders"); + assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "History"); assert_eq!( radarr_data.main_tabs.tabs[4].route, - ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::History.into() ); assert!(radarr_data.main_tabs.tabs[4].contextual_help.is_some()); assert_eq!( radarr_data.main_tabs.tabs[4].contextual_help.unwrap(), - &ROOT_FOLDERS_CONTEXT_CLUES + &HISTORY_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[4].config, None); - assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "Indexers"); + assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "Root Folders"); assert_eq!( radarr_data.main_tabs.tabs[5].route, - ActiveRadarrBlock::Indexers.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(radarr_data.main_tabs.tabs[5].contextual_help.is_some()); assert_eq!( radarr_data.main_tabs.tabs[5].contextual_help.unwrap(), - &INDEXERS_CONTEXT_CLUES + &ROOT_FOLDERS_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[5].config, None); - assert_str_eq!(radarr_data.main_tabs.tabs[6].title, "System"); + assert_str_eq!(radarr_data.main_tabs.tabs[6].title, "Indexers"); assert_eq!( radarr_data.main_tabs.tabs[6].route, - ActiveRadarrBlock::System.into() + ActiveRadarrBlock::Indexers.into() ); assert!(radarr_data.main_tabs.tabs[6].contextual_help.is_some()); assert_eq!( radarr_data.main_tabs.tabs[6].contextual_help.unwrap(), - &SYSTEM_CONTEXT_CLUES + &INDEXERS_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[6].config, None); + assert_str_eq!(radarr_data.main_tabs.tabs[7].title, "System"); + assert_eq!( + radarr_data.main_tabs.tabs[7].route, + ActiveRadarrBlock::System.into() + ); + assert!(radarr_data.main_tabs.tabs[7].contextual_help.is_some()); + assert_eq!( + radarr_data.main_tabs.tabs[7].contextual_help.unwrap(), + &SYSTEM_CONTEXT_CLUES + ); + assert_eq!(radarr_data.main_tabs.tabs[7].config, None); + assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6); assert_str_eq!(radarr_data.movie_info_tabs.tabs[0].title, "Details"); @@ -334,8 +347,8 @@ mod tests { DELETE_MOVIE_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, - INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, LIBRARY_BLOCKS, - MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS, + HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, + LIBRARY_BLOCKS, MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS, }; #[test] @@ -388,6 +401,18 @@ mod tests { assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistSortPrompt)); } + #[test] + fn test_history_blocks_contents() { + assert_eq!(HISTORY_BLOCKS.len(), 7); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::History)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::HistoryItemDetails)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::HistorySortPrompt)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::FilterHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::FilterHistoryError)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::SearchHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::SearchHistoryError)); + } + #[test] fn test_add_movie_blocks_contents() { assert_eq!(ADD_MOVIE_BLOCKS.len(), 10); diff --git a/src/network/radarr_network/history/mod.rs b/src/network/radarr_network/history/mod.rs new file mode 100644 index 0000000..429b7dd --- /dev/null +++ b/src/network/radarr_network/history/mod.rs @@ -0,0 +1,63 @@ +use crate::models::Route; +use crate::models::radarr_models::RadarrHistoryWrapper; +use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; +use crate::network::radarr_network::RadarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; +use serde_json::Value; + +#[cfg(test)] +#[path = "radarr_history_network_tests.rs"] +mod radarr_history_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::radarr_network) async fn get_radarr_history( + &mut self, + events: u64, + ) -> Result { + info!("Fetching all Radarr history events"); + let event = RadarrEvent::GetHistory(events); + + let params = format!("pageSize={events}&sortDirection=descending&sortKey=date"); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), RadarrHistoryWrapper>(request_props, |history_response, mut app| { + if !matches!( + app.get_current_route(), + Route::Radarr(ActiveRadarrBlock::HistorySortPrompt, _) + ) { + let mut history_vec = history_response.records; + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.radarr_data.history.set_items(history_vec); + app.data.radarr_data.history.apply_sorting_toggle(false); + } + }) + .await + } + + pub(in crate::network::radarr_network) async fn mark_radarr_history_item_as_failed( + &mut self, + history_item_id: i64, + ) -> Result { + info!("Marking the Radarr history item with ID: {history_item_id} as 'failed'"); + let event = RadarrEvent::MarkHistoryItemAsFailed(history_item_id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + None, + Some(format!("/{history_item_id}")), + None, + ) + .await; + + self + .handle_request::<(), Value>(request_props, |_, _| ()) + .await + } +} diff --git a/src/network/radarr_network/history/radarr_history_network_tests.rs b/src/network/radarr_network/history/radarr_history_network_tests.rs new file mode 100644 index 0000000..b11a1bd --- /dev/null +++ b/src/network/radarr_network/history/radarr_history_network_tests.rs @@ -0,0 +1,196 @@ +#[cfg(test)] +mod tests { + use crate::models::radarr_models::{RadarrHistoryItem, RadarrHistoryWrapper, RadarrSerdeable}; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::stateful_table::SortOption; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use crate::network::radarr_network::RadarrEvent; + use crate::network::radarr_network::radarr_network_test_utils::test_utils::radarr_history_item; + use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::json; + + #[rstest] + #[tokio::test] + async fn test_handle_get_radarr_history_event(#[values(true, false)] use_custom_sorting: bool) { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z movie", + "movieId": 1007, + "quality": { "quality": { "name": "HD - 1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2022-12-30T07:37:56Z", + "eventType": "grabbed", + "data": { + "indexer": "DrunkenSlug (Prowlarr)", + "releaseGroup": "SPARKS", + "downloadClient": "transmission", + } + }, + { + "id": 456, + "sourceTitle": "A Movie", + "movieId": 2001, + "quality": { "quality": { "name": "HD - 1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2022-12-30T07:37:56Z", + "eventType": "grabbed", + "data": { + "indexer": "DrunkenSlug (Prowlarr)", + "releaseGroup": "SPARKS", + "downloadClient": "transmission", + } + }]}); + let response: RadarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let mut expected_history_items = vec![ + RadarrHistoryItem { + id: 123, + movie_id: 1007, + source_title: "z movie".into(), + ..radarr_history_item() + }, + RadarrHistoryItem { + id: 456, + movie_id: 2001, + source_title: "A Movie".into(), + ..radarr_history_item() + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("pageSize=500&sortDirection=descending&sortKey=date") + .build_for(RadarrEvent::GetHistory(500)) + .await; + app.lock().await.data.radarr_data.history.sort_asc = true; + if use_custom_sorting { + let cmp_fn = |a: &RadarrHistoryItem, b: &RadarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + expected_history_items.sort_by(cmp_fn); + + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .radarr_data + .history + .sorting(vec![history_sort_option]); + } + let mut network = test_network(&app); + + let RadarrSerdeable::HistoryWrapper(history) = network + .handle_radarr_event(RadarrEvent::GetHistory(500)) + .await + .unwrap() + else { + panic!("Expected HistoryWrapper") + }; + mock.assert_async().await; + assert_eq!( + app.lock().await.data.radarr_data.history.items, + expected_history_items + ); + assert!(app.lock().await.data.radarr_data.history.sort_asc); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_radarr_history_event_no_op_when_user_is_selecting_sort_options() { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z movie", + "movieId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "indexer": "DrunkenSlug (Prowlarr)", + "releaseGroup": "SPARKS" + } + }, + { + "id": 456, + "sourceTitle": "A Movie", + "movieId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "indexer": "DrunkenSlug (Prowlarr)", + "releaseGroup": "SPARKS" + } + }]}); + let response: RadarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("pageSize=500&sortDirection=descending&sortKey=date") + .build_for(RadarrEvent::GetHistory(500)) + .await; + app.lock().await.data.radarr_data.history.sort_asc = true; + app + .lock() + .await + .push_navigation_stack(ActiveRadarrBlock::HistorySortPrompt.into()); + let cmp_fn = |a: &RadarrHistoryItem, b: &RadarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .radarr_data + .history + .sorting(vec![history_sort_option]); + let mut network = test_network(&app); + + let RadarrSerdeable::HistoryWrapper(history) = network + .handle_radarr_event(RadarrEvent::GetHistory(500)) + .await + .unwrap() + else { + panic!("Expected HistoryWrapper") + }; + mock.assert_async().await; + assert_is_empty!(app.lock().await.data.radarr_data.history); + assert!(app.lock().await.data.radarr_data.history.sort_asc); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_mark_radarr_history_item_as_failed_event() { + let expected_history_item_id = 1; + let (mock, app, _server) = MockServarrApi::post() + .returns(json!({})) + .path("/1") + .build_for(RadarrEvent::MarkHistoryItemAsFailed( + expected_history_item_id, + )) + .await; + let mut network = test_network(&app); + + let result = network + .handle_radarr_event(RadarrEvent::MarkHistoryItemAsFailed( + expected_history_item_id, + )) + .await; + + mock.assert_async().await; + assert_ok!(result); + } +} diff --git a/src/network/radarr_network/mod.rs b/src/network/radarr_network/mod.rs index 72ff7d9..207a4ca 100644 --- a/src/network/radarr_network/mod.rs +++ b/src/network/radarr_network/mod.rs @@ -16,6 +16,7 @@ use super::NetworkResource; mod blocklist; mod collections; mod downloads; +mod history; mod indexers; mod library; mod root_folders; @@ -47,10 +48,12 @@ pub enum RadarrEvent { GetBlocklist, GetCollections, GetDownloads(u64), + GetHistory(u64), GetHostConfig, GetIndexers, GetAllIndexerSettings, GetLogs(u64), + MarkHistoryItemAsFailed(i64), GetMovieCredits(i64), GetMovieDetails(i64), GetMovieHistory(i64), @@ -86,7 +89,9 @@ impl NetworkResource for RadarrEvent { RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", RadarrEvent::GetCollections | RadarrEvent::EditCollection(_) => "/collection", RadarrEvent::GetDownloads(_) | RadarrEvent::DeleteDownload(_) => "/queue", + RadarrEvent::GetHistory(_) => "/history", RadarrEvent::GetHostConfig | RadarrEvent::GetSecurityConfig => "/config/host", + RadarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", RadarrEvent::GetIndexers | RadarrEvent::EditIndexer(_) | RadarrEvent::DeleteIndexer(_) => { "/indexer" } @@ -199,6 +204,10 @@ impl Network<'_, '_> { .get_radarr_downloads(count) .await .map(RadarrSerdeable::from), + RadarrEvent::GetHistory(events) => self + .get_radarr_history(events) + .await + .map(RadarrSerdeable::from), RadarrEvent::GetHostConfig => self .get_radarr_host_config() .await @@ -208,6 +217,10 @@ impl Network<'_, '_> { .get_radarr_logs(events) .await .map(RadarrSerdeable::from), + RadarrEvent::MarkHistoryItemAsFailed(history_item_id) => self + .mark_radarr_history_item_as_failed(history_item_id) + .await + .map(RadarrSerdeable::from), RadarrEvent::GetMovieCredits(movie_id) => { self.get_credits(movie_id).await.map(RadarrSerdeable::from) } diff --git a/src/network/radarr_network/radarr_network_test_utils.rs b/src/network/radarr_network/radarr_network_test_utils.rs index 8cc37d6..b361f34 100644 --- a/src/network/radarr_network/radarr_network_test_utils.rs +++ b/src/network/radarr_network/radarr_network_test_utils.rs @@ -3,8 +3,8 @@ pub mod test_utils { use crate::models::radarr_models::{ AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit, CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability, - Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, - Rating, RatingsList, + Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData, RadarrHistoryEventType, + RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating, RatingsList, }; use crate::models::servarr_models::{ Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, @@ -313,6 +313,24 @@ pub mod test_utils { } } + pub fn radarr_history_item() -> RadarrHistoryItem { + RadarrHistoryItem { + id: 1, + source_title: HorizontallyScrollableText::from("Test"), + movie_id: 1, + quality: quality_wrapper(), + languages: vec![language()], + date: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()), + event_type: RadarrHistoryEventType::Grabbed, + data: RadarrHistoryData { + indexer: Some("DrunkenSlug (Prowlarr)".to_owned()), + release_group: Some("SPARKS".to_owned()), + download_client: Some("transmission".to_owned()), + ..RadarrHistoryData::default() + }, + } + } + pub fn download_record() -> DownloadRecord { DownloadRecord { title: "Test Download Title".to_owned(), diff --git a/src/network/radarr_network/radarr_network_tests.rs b/src/network/radarr_network/radarr_network_tests.rs index cfa10b0..e6f2c54 100644 --- a/src/network/radarr_network/radarr_network_tests.rs +++ b/src/network/radarr_network/radarr_network_tests.rs @@ -136,7 +136,9 @@ mod test { #[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")] #[case(RadarrEvent::DeleteBlocklistItem(1), "/blocklist")] #[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] + #[case(RadarrEvent::GetHistory(500), "/history")] #[case(RadarrEvent::GetLogs(500), "/log")] + #[case(RadarrEvent::MarkHistoryItemAsFailed(1), "/history/failed")] #[case(RadarrEvent::SearchNewMovie(String::new()), "/movie/lookup")] #[case(RadarrEvent::GetMovieCredits(0), "/credit")] #[case(RadarrEvent::GetMovieHistory(0), "/history/movie")] diff --git a/src/ui/radarr_ui/history/history_ui_tests.rs b/src/ui/radarr_ui/history/history_ui_tests.rs new file mode 100644 index 0000000..99b7196 --- /dev/null +++ b/src/ui/radarr_ui/history/history_ui_tests.rs @@ -0,0 +1,79 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::radarr_ui::history::HistoryUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_history_ui_accepts() { + ActiveRadarrBlock::iter().for_each(|active_radarr_block| { + if HISTORY_BLOCKS.contains(&active_radarr_block) { + assert!(HistoryUi::accepts(active_radarr_block.into())); + } else { + assert!(!HistoryUi::accepts(active_radarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_history_ui_renders_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_history_ui_renders_empty( + #[values(ActiveRadarrBlock::History, ActiveRadarrBlock::HistoryItemDetails)] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_radarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("loading_history_tab_{active_radarr_block}"), output); + } + + #[rstest] + fn test_history_ui_renders( + #[values( + ActiveRadarrBlock::History, + ActiveRadarrBlock::HistoryItemDetails, + ActiveRadarrBlock::HistorySortPrompt, + ActiveRadarrBlock::FilterHistory, + ActiveRadarrBlock::FilterHistoryError, + ActiveRadarrBlock::SearchHistory, + ActiveRadarrBlock::SearchHistoryError + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_radarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("history_tab_{active_radarr_block}"), output); + } + } +} diff --git a/src/ui/radarr_ui/history/mod.rs b/src/ui/radarr_ui/history/mod.rs new file mode 100644 index 0000000..797ee58 --- /dev/null +++ b/src/ui/radarr_ui/history/mod.rs @@ -0,0 +1,129 @@ +use crate::app::App; +use crate::models::Route; +use crate::models::radarr_models::RadarrHistoryItem; +use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS}; +use crate::ui::DrawUi; +use crate::ui::styles::{ManagarrStyle, secondary_style}; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use ratatui::Frame; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::text::Text; +use ratatui::widgets::{Cell, Row}; + +use super::radarr_ui_utils::create_history_event_details; + +#[cfg(test)] +#[path = "history_ui_tests.rs"] +mod history_ui_tests; + +pub(super) struct HistoryUi; + +impl DrawUi for HistoryUi { + fn accepts(route: Route) -> bool { + if let Route::Radarr(active_radarr_block, _) = route { + return HISTORY_BLOCKS.contains(&active_radarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + draw_history_table(f, app, area); + + if active_radarr_block == ActiveRadarrBlock::HistoryItemDetails { + draw_history_item_details_popup(f, app); + } + } + } +} + +fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = if app.data.radarr_data.history.items.is_empty() { + RadarrHistoryItem::default() + } else { + app.data.radarr_data.history.current_selection().clone() + }; + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + let history_row_mapping = |history_item: &RadarrHistoryItem| { + let RadarrHistoryItem { + source_title, + languages, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.ui_scroll_tick_count == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from( + languages + .iter() + .map(|language| language.name.to_owned()) + .collect::>() + .join(","), + ), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let history_table = + ManagarrTable::new(Some(&mut app.data.radarr_data.history), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_radarr_block == ActiveRadarrBlock::HistorySortPrompt) + .searching(active_radarr_block == ActiveRadarrBlock::SearchHistory) + .search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchHistoryError) + .filtering(active_radarr_block == ActiveRadarrBlock::FilterHistory) + .filter_produced_empty_results(active_radarr_block == ActiveRadarrBlock::FilterHistoryError) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); + + if [ + ActiveRadarrBlock::SearchHistory, + ActiveRadarrBlock::FilterHistory, + ] + .contains(&active_radarr_block) + { + history_table.show_cursor(f, area); + } + + f.render_widget(history_table, area); + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = if app.data.radarr_data.history.items.is_empty() { + RadarrHistoryItem::default() + } else { + app.data.radarr_data.history.current_selection().clone() + }; + + let line_vec = create_history_event_details(current_selection); + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(secondary_style()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); +} diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap new file mode 100644 index 0000000..08eb332 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + + + + + + ╭───────────────── Filter ──────────────────╮ + │Something │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap new file mode 100644 index 0000000..b2bc3c7 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │The given filter produced empty results│ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap new file mode 100644 index 0000000..a50a68b --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap new file mode 100644 index 0000000..a33398c --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap @@ -0,0 +1,39 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + ╭─────────────────────────────────── Details ───────────────────────────────────╮ + │Source Title: Test │ + │Event Type: grabbed │ + │Indexer: DrunkenSlug (Prowlarr) │ + │Release Group: SPARKS │ + │NZB Info URL: │ + │Download Client: transmission │ + │Age: 0 days │ + │Published Date: 1970-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap new file mode 100644 index 0000000..2839cc9 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + ╭───────────────────────────────╮ + │Something │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap new file mode 100644 index 0000000..9097f94 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + + + + + + ╭───────────────── Search ──────────────────╮ + │Something │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap new file mode 100644 index 0000000..ef6ccda --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │ No items found matching search │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap new file mode 100644 index 0000000..2b84f5c --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap new file mode 100644 index 0000000..c9acf0e --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap new file mode 100644 index 0000000..6d3c9e9 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap @@ -0,0 +1,39 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + ╭─────────────────────────────────── Details ───────────────────────────────────╮ + │Source Title: │ + │Event Type: unknown │ + │ │ + │No additional data available │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 8c6fbe6..882d85a 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -21,6 +21,7 @@ use crate::ui::draw_tabs; use crate::ui::radarr_ui::blocklist::BlocklistUi; use crate::ui::radarr_ui::collections::CollectionsUi; use crate::ui::radarr_ui::downloads::DownloadsUi; +use crate::ui::radarr_ui::history::HistoryUi; use crate::ui::radarr_ui::indexers::IndexersUi; use crate::ui::radarr_ui::library::LibraryUi; use crate::ui::radarr_ui::root_folders::RootFoldersUi; @@ -35,10 +36,12 @@ use crate::utils::convert_to_gb; mod blocklist; mod collections; mod downloads; +mod history; mod indexers; mod library; #[cfg(test)] mod radarr_ui_tests; +mod radarr_ui_utils; mod root_folders; mod system; @@ -61,6 +64,7 @@ impl DrawUi for RadarrUi { _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), _ if SystemUi::accepts(route) => SystemUi::draw(f, app, content_area), _ if BlocklistUi::accepts(route) => BlocklistUi::draw(f, app, content_area), + _ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area), _ => (), } } diff --git a/src/ui/radarr_ui/radarr_ui_tests.rs b/src/ui/radarr_ui/radarr_ui_tests.rs index ab43f32..4c0838f 100644 --- a/src/ui/radarr_ui/radarr_ui_tests.rs +++ b/src/ui/radarr_ui/radarr_ui_tests.rs @@ -29,9 +29,10 @@ mod tests { #[case(ActiveRadarrBlock::Collections, 1)] #[case(ActiveRadarrBlock::Downloads, 2)] #[case(ActiveRadarrBlock::Blocklist, 3)] - #[case(ActiveRadarrBlock::RootFolders, 4)] - #[case(ActiveRadarrBlock::Indexers, 5)] - #[case(ActiveRadarrBlock::System, 6)] + #[case(ActiveRadarrBlock::History, 4)] + #[case(ActiveRadarrBlock::RootFolders, 5)] + #[case(ActiveRadarrBlock::Indexers, 6)] + #[case(ActiveRadarrBlock::System, 7)] fn test_radarr_ui_renders_radarr_tabs( #[case] active_radarr_block: ActiveRadarrBlock, #[case] index: usize, 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..ac51c5b --- /dev/null +++ b/src/ui/radarr_ui/radarr_ui_utils.rs @@ -0,0 +1,121 @@ +use ratatui::text::Line; + +use crate::models::radarr_models::{RadarrHistoryData, RadarrHistoryEventType, RadarrHistoryItem}; + +#[cfg(test)] +#[path = "radarr_ui_utils_tests.rs"] +mod radarr_ui_utils_tests; + +pub(super) fn create_history_event_details(history_item: RadarrHistoryItem) -> Vec> { + let RadarrHistoryItem { + source_title, + event_type, + data, + .. + } = history_item; + let RadarrHistoryData { + indexer, + release_group, + nzb_info_url, + download_client, + download_client_name, + age, + published_date, + dropped_path, + imported_path, + message, + reason, + source_path, + path, + .. + } = data; + + let mut lines = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {}", event_type)), + ]; + + match event_type { + RadarrHistoryEventType::Grabbed => { + lines.push(Line::from(format!( + "Indexer: {}", + indexer.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "NZB Info URL: {}", + nzb_info_url.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Download Client: {}", + download_client + .or(download_client_name) + .unwrap_or_default() + .trim_start() + ))); + lines.push(Line::from(format!( + "Age: {} days", + age.unwrap_or("0".to_owned()).trim_start(), + ))); + lines.push(Line::from(format!( + "Published Date: {}", + published_date.unwrap_or_default().to_string().trim_start(), + ))); + } + RadarrHistoryEventType::DownloadFolderImported => { + lines.push(Line::from(format!( + "Dropped Path: {}", + dropped_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Imported Path: {}", + imported_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Download Client: {}", + download_client + .or(download_client_name) + .unwrap_or_default() + .trim_start() + ))); + } + RadarrHistoryEventType::DownloadFailed => { + lines.push(Line::from(format!( + "Download Client: {}", + download_client + .or(download_client_name) + .unwrap_or_default() + .trim_start(), + ))); + lines.push(Line::from(format!( + "Message: {}", + message.unwrap_or_default().trim_start(), + ))); + } + RadarrHistoryEventType::MovieFileDeleted => { + lines.push(Line::from(format!( + "Reason: {}", + reason.unwrap_or_default().trim_start(), + ))); + } + RadarrHistoryEventType::MovieFileRenamed => { + lines.push(Line::from(format!( + "Source Path: {}", + source_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Destination Path: {}", + path.unwrap_or_default().trim_start(), + ))); + } + _ => { + lines.push(Line::from(String::new())); + lines.push(Line::from("No additional data available")); + } + } + + lines +} diff --git a/src/ui/radarr_ui/radarr_ui_utils_tests.rs b/src/ui/radarr_ui/radarr_ui_utils_tests.rs new file mode 100644 index 0000000..070dafd --- /dev/null +++ b/src/ui/radarr_ui/radarr_ui_utils_tests.rs @@ -0,0 +1,236 @@ +#[cfg(test)] +mod tests { + use chrono::Utc; + use ratatui::text::Line; + + use crate::models::radarr_models::RadarrHistoryEventType; + use crate::models::radarr_models::{RadarrHistoryData, RadarrHistoryItem}; + use crate::ui::radarr_ui::radarr_ui_utils::create_history_event_details; + use pretty_assertions::assert_eq; + + #[test] + fn test_create_grabbed_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::Grabbed); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { + indexer, + release_group, + nzb_info_url, + download_client, + download_client_name, + age, + published_date, + .. + } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Indexer: {}", + indexer.unwrap_or_default().trim_start(), + )), + Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + )), + Line::from(format!( + "NZB Info URL: {}", + nzb_info_url.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Download Client: {}", + download_client + .or(download_client_name) + .unwrap_or_default() + .trim_start() + )), + Line::from(format!( + "Age: {} days", + age.unwrap_or("0".to_owned()).trim_start(), + )), + Line::from(format!( + "Published Date: {}", + published_date.unwrap_or_default().to_string().trim_start() + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_download_folder_imported_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::DownloadFolderImported); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { + dropped_path, + imported_path, + download_client, + .. + } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Dropped Path: {}", + dropped_path.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Imported Path: {}", + imported_path.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Download Client: {}", + download_client.unwrap_or_default().trim_start(), + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_download_failed_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::DownloadFailed); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { + message, + download_client, + .. + } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Download Client: {}", + download_client.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Message: {}", + message.unwrap_or_default().trim_start() + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_movie_file_deleted_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::MovieFileDeleted); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { reason, .. } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Reason: {}", + reason.unwrap_or_default().trim_start(), + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_movie_file_renamed_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::MovieFileRenamed); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { + source_path, path, .. + } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Source Path: {}", + source_path.unwrap_or_default().trim_start(), + )), + Line::from(format!( + "Destination Path: {}", + path.unwrap_or_default().trim_start(), + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_no_data_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::Unknown); + let RadarrHistoryItem { + source_title, + event_type, + .. + } = history_item.clone(); + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(String::new()), + Line::from("No additional data available"), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + fn radarr_history_item(event_type: RadarrHistoryEventType) -> RadarrHistoryItem { + RadarrHistoryItem { + source_title: "test.source.title".into(), + event_type, + data: radarr_history_data(), + ..RadarrHistoryItem::default() + } + } + + fn radarr_history_data() -> RadarrHistoryData { + RadarrHistoryData { + dropped_path: Some("/dropped/test".into()), + imported_path: Some("/imported/test".into()), + indexer: Some("Test Indexer".into()), + release_group: Some("test release group".into()), + nzb_info_url: Some("test url".into()), + download_client: Some("test download client".into()), + download_client_name: None, + age: Some("1".into()), + published_date: Some(Utc::now()), + message: Some("test message".into()), + reason: Some("test reason".into()), + source_path: Some("/source/path".into()), + path: Some("/path".into()), + } + } +} diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Blocklist.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Blocklist.snap index c502692..97b29eb 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Blocklist.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Blocklist.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Movie Title ▼ Source Title Languages Quality Formats Date │ │=> Test z movie English HD - 1080p English 2024-02-10 07:28:45 UTC │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Collections.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Collections.snap index 983cf62..87f3b8e 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Collections.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Collections.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Collection ▼ Number of Movies Root Folder Path Quality Profile Search on Add Monitored │ │=> Test Collection 1 /nfs/movies HD - 1080p Yes 🏷 │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Downloads.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Downloads.snap index 16641df..70721de 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Downloads.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Downloads.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Title Percent Complete Size Output Path Indexer Download Client │ │=> Test Download Title 50% 3.30 GB /nfs/movies/Test kickass torrents transmission │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__History.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__History.snap new file mode 100644 index 0000000..baa99cf --- /dev/null +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__History.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/radarr_ui/radarr_ui_tests.rs +expression: output +--- +╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Source Title ▼ Event Type Language Quality Date │ +│=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Indexers.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Indexers.snap index 0576c5c..b5aaaf8 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Indexers.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Indexers.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ 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/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Movies.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Movies.snap index 82fca3e..7e9fd6f 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Movies.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Movies.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Title ▼ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags │ │=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p 🏷 alex │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__RootFolders.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__RootFolders.snap index 10d0cfe..5ba427d 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__RootFolders.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__RootFolders.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Path Free Space Unmapped Folders │ │=> /nfs 204800.00 GB 0 │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__System.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__System.snap index 5f3c163..16c2abc 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__System.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__System.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │╭ Tasks ───────────────────────────────────────────────────────────────────────╮╭ Queued Events ──────────────────────────────────────────────────────────────╮│ ││Name Interval Last Execution Last Duration Next Execution ││Trigger Status Name Queued Started Duration ││ ││Backup 1 hour now 00:00:17 59 minutes ││manual completed Refresh Monitored 4 minutes ago 4 minutes a 00:03:03 ││ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap index 22a7c1a..9704700 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap @@ -16,7 +16,7 @@ expression: output │ ││ ││ │ ╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯ ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Title ▼ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags │ │=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p 🏷 alex │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap index dad6259..f0e42bc 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap @@ -19,7 +19,7 @@ expression: output │ ││ ││ │ ╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯ ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Title ▼ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags │ │=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p 🏷 alex │