From c624d1b9e4d15b99d958fe52664d3255e68f62e3 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 8 Jan 2026 15:16:01 -0700 Subject: [PATCH] feat: Full CLI and TUI support for adding an artist to Lidarr --- src/app/lidarr/lidarr_context_clues.rs | 16 +- src/cli/lidarr/add_command_handler.rs | 93 +- src/cli/lidarr/add_command_handler_tests.rs | 370 ++++- .../library/add_artist_handler.rs | 521 ++++++- .../library/add_artist_handler_tests.rs | 1353 ++++++++++++++++- .../library/add_series_handler.rs | 8 +- .../library/add_series_handler_tests.rs | 11 +- src/models/lidarr_models.rs | 57 + src/models/lidarr_models_tests.rs | 26 +- src/models/servarr_data/lidarr/lidarr_data.rs | 54 +- .../servarr_data/lidarr/lidarr_data_tests.rs | 50 +- src/models/servarr_data/lidarr/modals.rs | 39 +- .../servarr_data/lidarr/modals_tests.rs | 61 +- .../library/lidarr_library_network_tests.rs | 105 +- src/network/lidarr_network/library/mod.rs | 28 +- .../lidarr_network/lidarr_network_tests.rs | 3 +- src/network/lidarr_network/mod.rs | 5 +- src/ui/lidarr_ui/library/add_artist_ui.rs | 272 +++- .../lidarr_ui/library/add_artist_ui_tests.rs | 65 +- ..._artist_already_in_library_ui_renders.snap | 47 + ...d_artist_modal_AddArtistConfirmPrompt.snap | 14 + ...sts__add_artist_modal_AddArtistPrompt.snap | 48 + ..._modal_AddArtistSelectMetadataProfile.snap | 48 + ...d_artist_modal_AddArtistSelectMonitor.snap | 48 + ..._modal_AddArtistSelectMonitorNewItems.snap | 48 + ...t_modal_AddArtistSelectQualityProfile.snap | 48 + ...rtist_modal_AddArtistSelectRootFolder.snap | 48 + ...__add_artist_modal_AddArtistTagsInput.snap | 48 + 28 files changed, 3448 insertions(+), 86 deletions(-) create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_already_in_library_ui_renders.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistConfirmPrompt.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistPrompt.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMetadataProfile.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitor.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitorNewItems.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectQualityProfile.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectRootFolder.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistTagsInput.snap diff --git a/src/app/lidarr/lidarr_context_clues.rs b/src/app/lidarr/lidarr_context_clues.rs index 88e5ea5..7262e93 100644 --- a/src/app/lidarr/lidarr_context_clues.rs +++ b/src/app/lidarr/lidarr_context_clues.rs @@ -1,8 +1,12 @@ use crate::app::App; -use crate::app::context_clues::{BARE_POPUP_CONTEXT_CLUES, ContextClue, ContextClueProvider}; +use crate::app::context_clues::{ + BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, +}; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::models::Route; -use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock}; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, +}; #[cfg(test)] #[path = "lidarr_context_clues_tests.rs"] @@ -47,6 +51,14 @@ impl ContextClueProvider for LidarrContextClueProvider { _ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) => { Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES) } + ActiveLidarrBlock::AddArtistPrompt + | ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder + | ActiveLidarrBlock::AddArtistTagsInput + | ActiveLidarrBlock::AddArtistAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES), _ if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) => { Some(&ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES) } diff --git a/src/cli/lidarr/add_command_handler.rs b/src/cli/lidarr/add_command_handler.rs index 5ffc0f2..3aeb718 100644 --- a/src/cli/lidarr/add_command_handler.rs +++ b/src/cli/lidarr/add_command_handler.rs @@ -1,13 +1,14 @@ use std::sync::Arc; use anyhow::Result; -use clap::{Subcommand, arg}; +use clap::{ArgAction, Subcommand, arg}; use tokio::sync::Mutex; use super::LidarrCommand; use crate::{ app::App, cli::{CliCommandHandler, Command}, + models::lidarr_models::{AddArtistBody, AddArtistOptions, MonitorType, NewItemMonitorType}, network::{NetworkTrait, lidarr_network::LidarrEvent}, }; @@ -17,6 +18,63 @@ mod add_command_handler_tests; #[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum LidarrAddCommand { + #[command(about = "Add a new artist to your Lidarr library")] + Artist { + #[arg( + long, + help = "The MusicBrainz foreign artist ID of the artist you wish to add to your library", + required = true + )] + foreign_artist_id: String, + #[arg(long, help = "The name of the artist", required = true)] + artist_name: String, + #[arg( + long, + help = "The root folder path where all artist data and metadata should live", + required = true + )] + root_folder_path: String, + #[arg( + long, + help = "The ID of the quality profile to use for this artist", + required = true + )] + quality_profile_id: i64, + #[arg( + long, + help = "The ID of the metadata profile to use for this artist", + required = true + )] + metadata_profile_id: i64, + #[arg(long, help = "Disable monitoring for this artist")] + disable_monitoring: bool, + #[arg( + long, + help = "Tag IDs to tag the artist with", + value_parser, + action = ArgAction::Append + )] + tag: Vec, + #[arg( + long, + help = "What Lidarr should monitor for this artist", + value_enum, + default_value_t = MonitorType::default() + )] + monitor: MonitorType, + #[arg( + long, + help = "How Lidarr should monitor new items for this artist", + value_enum, + default_value_t = NewItemMonitorType::default() + )] + monitor_new_items: NewItemMonitorType, + #[arg( + long, + help = "Tell Lidarr to not start a search for missing albums once the artist is added to your library" + )] + no_search_for_missing_albums: bool, + }, #[command(about = "Add new tag")] Tag { #[arg(long, help = "The name of the tag to be added", required = true)] @@ -51,6 +109,39 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHan async fn handle(self) -> Result { let result = match self.command { + LidarrAddCommand::Artist { + foreign_artist_id, + artist_name, + root_folder_path, + quality_profile_id, + metadata_profile_id, + disable_monitoring, + tag: tags, + monitor, + monitor_new_items, + no_search_for_missing_albums, + } => { + let body = AddArtistBody { + foreign_artist_id, + artist_name, + monitored: !disable_monitoring, + root_folder_path, + quality_profile_id, + metadata_profile_id, + tags, + tag_input_string: None, + add_options: AddArtistOptions { + monitor, + monitor_new_items, + search_for_missing_albums: !no_search_for_missing_albums, + }, + }; + let resp = self + .network + .handle_network_event(LidarrEvent::AddArtist(body).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrAddCommand::Tag { name } => { let resp = self .network diff --git a/src/cli/lidarr/add_command_handler_tests.rs b/src/cli/lidarr/add_command_handler_tests.rs index 351fee3..5f24baf 100644 --- a/src/cli/lidarr/add_command_handler_tests.rs +++ b/src/cli/lidarr/add_command_handler_tests.rs @@ -8,6 +8,7 @@ mod tests { Command, lidarr::{LidarrCommand, add_command_handler::LidarrAddCommand}, }, + models::lidarr_models::{MonitorType, NewItemMonitorType}, }; use pretty_assertions::assert_eq; @@ -52,6 +53,321 @@ mod tests { }; assert_eq!(add_command, expected_args); } + + #[test] + fn test_add_artist_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "artist"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_foreign_artist_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--artist-name", + "Test", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_artist_name() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_root_folder_path() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_quality_profile_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test", + "--root-folder-path", + "/music", + "--metadata-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_metadata_profile_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_success_with_required_args_only() { + let expected_args = LidarrAddCommand::Artist { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + disable_monitoring: false, + tag: vec![], + monitor: MonitorType::default(), + monitor_new_items: NewItemMonitorType::default(), + no_search_for_missing_albums: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type") + }; + assert_eq!(add_command, expected_args); + } + + #[test] + fn test_add_artist_success_with_all_args() { + let expected_args = LidarrAddCommand::Artist { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 2, + disable_monitoring: true, + tag: vec![1, 2], + monitor: MonitorType::Future, + monitor_new_items: NewItemMonitorType::New, + no_search_for_missing_albums: true, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "2", + "--disable-monitoring", + "--tag", + "1", + "--tag", + "2", + "--monitor", + "future", + "--monitor-new-items", + "new", + "--no-search-for-missing-albums", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type") + }; + assert_eq!(add_command, expected_args); + } + + #[test] + fn test_add_artist_monitor_type_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "2", + "--monitor", + "test", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_add_artist_new_item_monitor_type_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "2", + "--monitor-new-items", + "test", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_add_artist_tags_is_repeatable() { + let expected_args = LidarrAddCommand::Artist { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 2, + disable_monitoring: false, + tag: vec![1, 2], + monitor: MonitorType::default(), + monitor_new_items: NewItemMonitorType::default(), + no_search_for_missing_albums: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "2", + "--tag", + "1", + "--tag", + "2", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type") + }; + assert_eq!(add_command, expected_args); + } } mod handler { @@ -64,7 +380,9 @@ mod tests { use crate::cli::CliCommandHandler; use crate::cli::lidarr::add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler}; use crate::models::Serdeable; - use crate::models::lidarr_models::LidarrSerdeable; + use crate::models::lidarr_models::{ + AddArtistBody, AddArtistOptions, LidarrSerdeable, MonitorType, NewItemMonitorType, + }; use crate::network::lidarr_network::LidarrEvent; use crate::{ app::App, @@ -97,5 +415,55 @@ mod tests { assert_ok!(&result); } + + #[tokio::test] + async fn test_handle_add_artist_command() { + let expected_body = AddArtistBody { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + monitored: false, + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: vec![1, 2], + tag_input_string: None, + add_options: AddArtistOptions { + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + search_for_missing_albums: false, + }, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::AddArtist(expected_body).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let add_artist_command = LidarrAddCommand::Artist { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + disable_monitoring: true, + tag: vec![1, 2], + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + no_search_for_missing_albums: true, + }; + + let result = LidarrAddCommandHandler::with(&app_arc, add_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } } } diff --git a/src/handlers/lidarr_handlers/library/add_artist_handler.rs b/src/handlers/lidarr_handlers/library/add_artist_handler.rs index 39b8fae..7f4e63b 100644 --- a/src/handlers/lidarr_handlers/library/add_artist_handler.rs +++ b/src/handlers/lidarr_handlers/library/add_artist_handler.rs @@ -1,8 +1,13 @@ -use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; -use crate::models::Route; -use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock}; -use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys}; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions, AddArtistSearchResult}; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ActiveLidarrBlock, +}; +use crate::models::servarr_data::lidarr::modals::AddArtistModal; +use crate::models::{BlockSelectionState, Route, Scrollable}; +use crate::network::lidarr_network::LidarrEvent; +use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; #[cfg(test)] #[path = "add_artist_handler_tests.rs"] @@ -15,6 +20,84 @@ pub struct AddArtistHandler<'a, 'b> { _context: Option, } +impl AddArtistHandler<'_, '_> { + fn build_add_artist_body(&mut self) -> AddArtistBody { + let add_artist_modal = self + .app + .data + .lidarr_data + .add_artist_modal + .take() + .expect("AddArtistModal is None"); + let tags = add_artist_modal.tags.text; + let AddArtistModal { + root_folder_list, + monitor_list, + monitor_new_items_list, + quality_profile_list, + metadata_profile_list, + .. + } = add_artist_modal; + let (foreign_artist_id, artist_name) = { + let AddArtistSearchResult { + foreign_artist_id, + artist_name, + .. + } = self + .app + .data + .lidarr_data + .add_searched_artists + .as_ref() + .unwrap() + .current_selection(); + (foreign_artist_id.clone(), artist_name.text.clone()) + }; + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self + .app + .data + .lidarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + let metadata_profile = metadata_profile_list.current_selection(); + let metadata_profile_id = *self + .app + .data + .lidarr_data + .metadata_profile_map + .iter() + .filter(|(_, value)| *value == metadata_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + let path = root_folder_list.current_selection().path.clone(); + let monitor = *monitor_list.current_selection(); + let monitor_new_items = *monitor_new_items_list.current_selection(); + + AddArtistBody { + foreign_artist_id, + artist_name, + monitored: true, + root_folder_path: path, + quality_profile_id, + metadata_profile_id, + tags: Vec::new(), + tag_input_string: Some(tags), + add_options: AddArtistOptions { + monitor, + monitor_new_items, + search_for_missing_albums: true, + }, + } + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a, 'b> { fn handle(&mut self) { let add_artist_table_handling_config = @@ -66,74 +149,382 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a, !self.app.is_loading } - fn handle_scroll_up(&mut self) {} + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_up(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_up(), + ActiveLidarrBlock::AddArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_up(), + ActiveLidarrBlock::AddArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_up(), + ActiveLidarrBlock::AddArtistSelectRootFolder => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_up(), + ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.up(), + _ => (), + } + } - fn handle_scroll_down(&mut self) {} + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_down(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_down(), + ActiveLidarrBlock::AddArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_down(), + ActiveLidarrBlock::AddArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_down(), + ActiveLidarrBlock::AddArtistSelectRootFolder => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_down(), + ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.down(), + _ => (), + } + } fn handle_home(&mut self) { - if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput { - self + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSelectRootFolder => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSearchInput => self .app .data .lidarr_data .add_artist_search .as_mut() .unwrap() - .scroll_home(); + .scroll_home(), + ActiveLidarrBlock::AddArtistTagsInput => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .tags + .scroll_home(), + _ => (), } } fn handle_end(&mut self) { - if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput { - self + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSelectRootFolder => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSearchInput => self .app .data .lidarr_data .add_artist_search .as_mut() .unwrap() - .reset_offset(); + .reset_offset(), + ActiveLidarrBlock::AddArtistTagsInput => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .tags + .reset_offset(), + _ => (), } } fn handle_delete(&mut self) {} fn handle_left_right_action(&mut self) { - if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput { - handle_text_box_left_right_keys!( - self, - self.key, - self - .app - .data - .lidarr_data - .add_artist_search - .as_mut() - .unwrap() - ) + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistPrompt => handle_prompt_toggle(self.app, self.key), + ActiveLidarrBlock::AddArtistSearchInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .add_artist_search + .as_mut() + .unwrap() + ) + } + ActiveLidarrBlock::AddArtistTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .tags + ) + } + _ => (), } } fn handle_submit(&mut self) { match self.active_lidarr_block { - ActiveLidarrBlock::AddArtistSearchInput => { - let search_text = &self + ActiveLidarrBlock::AddArtistSearchInput + if !self .app .data .lidarr_data .add_artist_search .as_ref() .unwrap() - .text; + .text + .is_empty() => + { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::AddArtistSearchResults + if self.app.data.lidarr_data.add_searched_artists.is_some() => + { + let foreign_artist_id = self + .app + .data + .lidarr_data + .add_searched_artists + .as_ref() + .unwrap() + .current_selection() + .foreign_artist_id + .clone(); - if !search_text.is_empty() { + if self + .app + .data + .lidarr_data + .artists + .items + .iter() + .any(|artist| artist.foreign_artist_id == foreign_artist_id) + { self .app - .push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); - self.app.ignore_special_keys_for_textbox_input = false; + .push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into()); + } else { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + self.app.data.lidarr_data.add_artist_modal = Some((&self.app.data.lidarr_data).into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); } } - ActiveLidarrBlock::AddArtistSearchResults => {} + ActiveLidarrBlock::AddArtistPrompt => { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::AddArtistConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::AddArtist(self.build_add_artist_body())); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.push_navigation_stack( + self + .app + .data + .lidarr_data + .selected_block + .get_active_block() + .into(), + ), + ActiveLidarrBlock::AddArtistTagsInput => { + self.app.push_navigation_stack( + self + .app + .data + .lidarr_data + .selected_block + .get_active_block() + .into(), + ); + self.app.ignore_special_keys_for_textbox_input = true; + } + _ => (), + } + } + ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(), + ActiveLidarrBlock::AddArtistTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } _ => (), } } @@ -151,23 +542,67 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a, self.app.data.lidarr_data.add_searched_artists = None; self.app.ignore_special_keys_for_textbox_input = true; } + ActiveLidarrBlock::AddArtistPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.add_artist_modal = None; + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistAlreadyInLibrary + | ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(), + ActiveLidarrBlock::AddArtistTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } _ => (), } } fn handle_char_key_event(&mut self) { - if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput { - handle_text_box_keys!( - self, - self.key, - self - .app - .data - .lidarr_data - .add_artist_search - .as_mut() - .unwrap() - ) + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSearchInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .add_artist_search + .as_mut() + .unwrap() + ) + } + ActiveLidarrBlock::AddArtistTagsInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .tags + ) + } + ActiveLidarrBlock::AddArtistPrompt => { + if self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::AddArtistConfirmPrompt + && matches_key!(confirm, key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::AddArtist(self.build_add_artist_body())); + self.app.pop_navigation_stack(); + } + } + _ => (), } } diff --git a/src/handlers/lidarr_handlers/library/add_artist_handler_tests.rs b/src/handlers/lidarr_handlers/library/add_artist_handler_tests.rs index dba8ca9..203089b 100644 --- a/src/handlers/lidarr_handlers/library/add_artist_handler_tests.rs +++ b/src/handlers/lidarr_handlers/library/add_artist_handler_tests.rs @@ -1,25 +1,677 @@ #[cfg(test)] mod tests { - use pretty_assertions::assert_str_eq; + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; use std::sync::atomic::Ordering; use strum::IntoEnumIterator; use crate::app::App; use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_modal_present; use crate::assert_navigation_popped; + use crate::assert_navigation_pushed; use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::lidarr_handlers::library::add_artist_handler::AddArtistHandler; - use crate::models::HorizontallyScrollableText; - use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock}; + use crate::models::lidarr_models::{ + AddArtistBody, AddArtistOptions, MonitorType, NewItemMonitorType, + }; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ActiveLidarrBlock, + }; + use crate::models::servarr_data::lidarr::modals::AddArtistModal; + use crate::models::servarr_models::RootFolder; use crate::models::stateful_table::StatefulTable; + use crate::models::{BlockSelectionState, HorizontallyScrollableText}; use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::add_artist_search_result; + use crate::simple_stateful_iterable_vec; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::ADD_ARTIST_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_add_artist_select_monitor_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_vec = Vec::from_iter(MonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_vec.len()).rev() { + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[i] + ); + } + } else { + for i in 0..monitor_vec.len() { + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[(i + 1) % monitor_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_artist_select_monitor_new_items_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_new_items_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(monitor_new_items_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_new_items_vec.len()).rev() { + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_new_items_vec[i] + ); + } + } else { + for i in 0..monitor_new_items_vec.len() { + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_new_items_vec[(i + 1) % monitor_new_items_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_artist_select_quality_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 2" + ); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_add_artist_select_metadata_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 2" + ); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_add_artist_select_root_folder_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .set_items(simple_stateful_iterable_vec!(RootFolder, String, path)); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 2" + ); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 1" + ); + } + + #[rstest] + fn test_add_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + AddArtistHandler::new(key, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddArtistSelectRootFolder + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems + ); + } + } + + #[rstest] + fn test_add_artist_prompt_scroll_no_op_when_not_ready(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_loading = true; + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + AddArtistHandler::new(key, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddArtistSelectMonitor + ); + } + } mod test_handle_home_end { - use super::*; use pretty_assertions::assert_eq; + use crate::extended_stateful_iterable_vec; + + use super::*; + + #[test] + fn test_add_artist_select_monitor_home_end() { + let monitor_vec = Vec::from_iter(MonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_vec.clone()); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[monitor_vec.len() - 1] + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[0] + ); + } + + #[test] + fn test_add_artist_select_monitor_new_items_home_end() { + let monitor_new_items_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(monitor_new_items_vec.clone()); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_new_items_vec[monitor_new_items_vec.len() - 1] + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_new_items_vec[0] + ); + } + + #[test] + fn test_add_artist_select_quality_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 3" + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_add_artist_select_metadata_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 3" + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_add_artist_select_root_folder_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .set_items(extended_stateful_iterable_vec!(RootFolder, String, path)); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 3" + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 1" + ); + } + #[test] fn test_add_artist_search_input_home_end_keys() { let mut app = App::test_default(); @@ -66,11 +718,79 @@ mod tests { 0 ); } + + #[test] + fn test_add_artist_tags_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal { + tags: "Test".into(), + ..AddArtistModal::default() + }); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } } mod test_handle_left_right_action { - use super::*; use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + AddArtistHandler::new(key, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + AddArtistHandler::new(key, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } #[test] fn test_add_artist_search_input_left_right_keys() { @@ -118,11 +838,67 @@ mod tests { 0 ); } + + #[test] + fn test_add_artist_tags_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal { + tags: "Test".into(), + ..AddArtistModal::default() + }); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } } mod test_handle_submit { use super::*; + use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist; use pretty_assertions::assert_eq; + use rstest::rstest; const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; @@ -142,10 +918,7 @@ mod tests { .handle(); assert!(!app.ignore_special_keys_for_textbox_input); - assert_eq!( - app.get_current_route(), - ActiveLidarrBlock::AddArtistSearchResults.into() - ); + assert_navigation_pushed!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); } #[test] @@ -170,13 +943,282 @@ mod tests { ActiveLidarrBlock::AddArtistSearchInput.into() ); } + + #[test] + fn test_add_artist_search_results_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddArtistPrompt.into() + ); + assert_modal_present!(app.data.lidarr_data.add_artist_modal); + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddArtistSelectRootFolder + ); + } + + #[test] + fn test_add_artist_search_results_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddArtistSearchResults.into() + ); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + } + + #[test] + fn test_add_artist_search_results_submit_does_nothing_on_empty_table() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddArtistSearchResults.into() + ); + } + + #[test] + fn test_add_artist_search_results_submit_artist_already_in_library() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + let mut add_searched_artists = StatefulTable::default(); + let search_result = add_artist_search_result(); + add_searched_artists.set_items(vec![search_result.clone()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + app.data.lidarr_data.artists.set_items(vec![artist()]); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::AddArtistAlreadyInLibrary.into()); + } + + #[test] + fn test_add_artist_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ARTIST_SELECTION_BLOCKS.len() - 1); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[test] + fn test_add_artist_confirm_prompt_prompt_confirm_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ARTIST_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.prompt_confirm = true; + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal { + tags: "usenet, testing".into(), + ..AddArtistModal::default() + }); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["Test".to_owned()]); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec!["Test".to_owned()]); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .set_items(vec![RootFolder { + path: "/music".to_owned(), + ..RootFolder::default() + }]); + let expected_add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_string(), + artist_name: "Test Artist".to_string(), + monitored: true, + root_folder_path: "/music".to_string(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddArtistOptions { + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + search_for_missing_albums: true, + }, + }; + let expected_lidarr_event = LidarrEvent::AddArtist(expected_add_artist_body); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_lidarr_event + ); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + } + + #[rstest] + #[case(ActiveLidarrBlock::AddArtistSelectRootFolder, 0)] + #[case(ActiveLidarrBlock::AddArtistSelectMonitor, 1)] + #[case(ActiveLidarrBlock::AddArtistSelectMonitorNewItems, 2)] + #[case(ActiveLidarrBlock::AddArtistSelectQualityProfile, 3)] + #[case(ActiveLidarrBlock::AddArtistSelectMetadataProfile, 4)] + #[case(ActiveLidarrBlock::AddArtistTagsInput, 5)] + fn test_add_artist_prompt_selected_block_submit( + #[case] block: ActiveLidarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, block.into()); + } + + #[rstest] + fn test_add_artist_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveLidarrBlock::AddArtistSelectRootFolder, + ActiveLidarrBlock::AddArtistSelectMonitor, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + ActiveLidarrBlock::AddArtistTagsInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + AddArtistHandler::new(SUBMIT_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistPrompt.into()); + + if active_lidarr_block == ActiveLidarrBlock::AddArtistTagsInput { + assert!(!app.ignore_special_keys_for_textbox_input); + } + } } mod test_handle_esc { - use super::*; - use crate::assert_modal_absent; use rstest::rstest; + use super::*; + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; #[rstest] @@ -222,10 +1264,88 @@ mod tests { assert_modal_absent!(app.data.lidarr_data.add_searched_artists); assert!(app.ignore_special_keys_for_textbox_input); } + + #[test] + fn test_add_artist_already_in_library_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into()); + + AddArtistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AddArtistAlreadyInLibrary, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + } + + #[test] + fn test_add_artist_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app.data.lidarr_data.prompt_confirm = true; + + AddArtistHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_add_artist_tags_input_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistTagsInput.into()); + app.ignore_special_keys_for_textbox_input = true; + + AddArtistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistPrompt.into()); + assert!(!app.ignore_special_keys_for_textbox_input); + } + + #[rstest] + fn test_add_artist_selecting_preferences_blocks_esc( + #[values( + ActiveLidarrBlock::AddArtistSelectMonitor, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + ActiveLidarrBlock::AddArtistSelectRootFolder + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + + AddArtistHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistPrompt.into()); + } } mod test_handle_key_char { use super::*; + use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions}; + use crate::network::lidarr_network::LidarrEvent; + use pretty_assertions::assert_str_eq; #[test] fn test_add_artist_search_input_backspace() { @@ -278,6 +1398,147 @@ mod tests { "a" ); } + + #[test] + fn test_add_artist_tags_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal { + tags: "Test".into(), + ..AddArtistModal::default() + }); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_add_artist_tags_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + + AddArtistHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .text, + "a" + ); + } + + #[test] + fn test_add_artist_confirm_prompt_confirm_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ARTIST_SELECTION_BLOCKS.len() - 1); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + let mut add_artist_modal = AddArtistModal { + tags: "usenet, testing".into(), + ..AddArtistModal::default() + }; + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_artist_modal + .quality_profile_list + .set_items(vec!["Test".to_owned()]); + add_artist_modal + .metadata_profile_list + .set_items(vec!["Test".to_owned()]); + add_artist_modal.root_folder_list.set_items(vec![ + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }, + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + add_artist_modal.root_folder_list.state.select(Some(1)); + app.data.lidarr_data.add_artist_modal = Some(add_artist_modal); + let expected_add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_string(), + artist_name: "Test Artist".to_string(), + monitored: true, + root_folder_path: "/nfs2".to_string(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddArtistOptions { + monitor: Default::default(), + monitor_new_items: Default::default(), + search_for_missing_albums: true, + }, + }; + let expected_lidarr_event = LidarrEvent::AddArtist(expected_add_artist_body); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_lidarr_event + ); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + } } #[test] @@ -353,4 +1614,74 @@ mod tests { assert!(handler.is_ready()); } + + #[test] + fn test_build_add_artist_body() { + let mut app = App::test_default(); + let mut add_artist_modal = AddArtistModal { + tags: "usenet, testing".into(), + ..AddArtistModal::default() + }; + add_artist_modal.root_folder_list.set_items(vec![ + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }, + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + add_artist_modal.root_folder_list.state.select(Some(1)); + add_artist_modal + .quality_profile_list + .set_items(vec!["Lossless".to_owned()]); + add_artist_modal + .metadata_profile_list + .set_items(vec!["Standard".to_owned()]); + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app.data.lidarr_data.add_artist_modal = Some(add_artist_modal); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Lossless".to_owned())]); + app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Standard".to_owned())]); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + let expected_add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_string(), + artist_name: "Test Artist".into(), + monitored: true, + root_folder_path: "/nfs2".to_string(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddArtistOptions { + monitor: Default::default(), + monitor_new_items: Default::default(), + search_for_missing_albums: true, + }, + }; + + let add_artist_body = AddArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .build_add_artist_body(); + + assert_eq!(add_artist_body, expected_add_artist_body); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + } } diff --git a/src/handlers/sonarr_handlers/library/add_series_handler.rs b/src/handlers/sonarr_handlers/library/add_series_handler.rs index 628c453..12af178 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler.rs @@ -426,8 +426,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, fn handle_submit(&mut self) { match self.active_sonarr_block { - _ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchInput - && !self + ActiveSonarrBlock::AddSeriesSearchInput + if !self .app .data .sonarr_data @@ -442,8 +442,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, .push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); self.app.ignore_special_keys_for_textbox_input = false; } - _ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchResults - && self.app.data.sonarr_data.add_searched_series.is_some() => + ActiveSonarrBlock::AddSeriesSearchResults + if self.app.data.sonarr_data.add_searched_series.is_some() => { let tvdb_id = self .app diff --git a/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs index 1003a5a..ee28d97 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs @@ -1030,8 +1030,6 @@ mod tests { app.is_loading = true; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); - let mut add_searched_series = StatefulTable::default(); - add_searched_series.set_items(vec![AddSeriesSearchResult::default()]); AddSeriesHandler::new( SUBMIT_KEY, @@ -1053,6 +1051,7 @@ mod tests { let mut app = App::test_default(); app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); + AddSeriesHandler::new( SUBMIT_KEY, &mut app, @@ -1092,7 +1091,7 @@ mod tests { } #[test] - fn test_add_series_prompt_prompt_decline_submit() { + fn test_add_series_confirm_prompt_prompt_decline_submit() { let mut app = App::test_default(); app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); @@ -1195,9 +1194,9 @@ mod tests { .handle(); assert_navigation_popped!(app, ActiveSonarrBlock::Series.into()); - assert_eq!( - app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::AddSeries(expected_add_series_body)) + assert_some_eq_x!( + &app.data.sonarr_data.prompt_confirm_action, + &SonarrEvent::AddSeries(expected_add_series_body.clone()) ); assert_modal_absent!(app.data.sonarr_data.add_series_modal); } diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index 21046ab..5724406 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -134,6 +134,40 @@ pub enum NewItemMonitorType { New, } +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + clap::ValueEnum, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum MonitorType { + #[default] + #[display_style(name = "All Albums")] + All, + #[display_style(name = "Future Albums")] + Future, + #[display_style(name = "Missing Albums")] + Missing, + #[display_style(name = "Existing Albums")] + Existing, + #[display_style(name = "First Album")] + First, + #[display_style(name = "Latest Album")] + Latest, + None, + Unknown, +} + #[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DownloadRecord { @@ -219,6 +253,29 @@ pub struct DeleteArtistParams { pub add_import_list_exclusion: bool, } +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AddArtistBody { + pub foreign_artist_id: String, + pub artist_name: String, + pub monitored: bool, + pub root_folder_path: String, + pub quality_profile_id: i64, + pub metadata_profile_id: i64, + pub tags: Vec, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, + pub add_options: AddArtistOptions, +} + +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AddArtistOptions { + pub monitor: MonitorType, + pub monitor_new_items: NewItemMonitorType, + pub search_for_missing_albums: bool, +} + #[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EditArtistParams { diff --git a/src/models/lidarr_models_tests.rs b/src/models/lidarr_models_tests.rs index 579f817..8108a6a 100644 --- a/src/models/lidarr_models_tests.rs +++ b/src/models/lidarr_models_tests.rs @@ -6,7 +6,7 @@ mod tests { use crate::models::lidarr_models::{ AddArtistSearchResult, DownloadRecord, DownloadStatus, DownloadsResponse, Member, - MetadataProfile, NewItemMonitorType, SystemStatus, + MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus, }; use crate::models::servarr_models::{ DiskSpace, HostConfig, QualityProfile, RootFolder, SecurityConfig, Tag, @@ -35,6 +35,30 @@ mod tests { assert_str_eq!(NewItemMonitorType::New.to_display_str(), "New Albums"); } + #[test] + fn test_monitor_type_display() { + assert_str_eq!(MonitorType::All.to_string(), "all"); + assert_str_eq!(MonitorType::Future.to_string(), "future"); + assert_str_eq!(MonitorType::Missing.to_string(), "missing"); + assert_str_eq!(MonitorType::Existing.to_string(), "existing"); + assert_str_eq!(MonitorType::First.to_string(), "first"); + assert_str_eq!(MonitorType::Latest.to_string(), "latest"); + assert_str_eq!(MonitorType::None.to_string(), "none"); + assert_str_eq!(MonitorType::Unknown.to_string(), "unknown"); + } + + #[test] + fn test_monitor_type_to_display_str() { + assert_str_eq!(MonitorType::All.to_display_str(), "All Albums"); + assert_str_eq!(MonitorType::Future.to_display_str(), "Future Albums"); + assert_str_eq!(MonitorType::Missing.to_display_str(), "Missing Albums"); + assert_str_eq!(MonitorType::Existing.to_display_str(), "Existing Albums"); + assert_str_eq!(MonitorType::First.to_display_str(), "First Album"); + assert_str_eq!(MonitorType::Latest.to_display_str(), "Latest Album"); + assert_str_eq!(MonitorType::None.to_display_str(), "None"); + assert_str_eq!(MonitorType::Unknown.to_display_str(), "Unknown"); + } + #[test] fn test_lidarr_serdeable_from() { let lidarr_serdeable = LidarrSerdeable::Value(json!({})); diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs index f8db53a..4211c40 100644 --- a/src/models/servarr_data/lidarr/lidarr_data.rs +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -1,6 +1,6 @@ use serde_json::Number; -use super::modals::EditArtistModal; +use super::modals::{AddArtistModal, EditArtistModal}; use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES; use crate::models::{ BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState, @@ -31,6 +31,7 @@ use { mod lidarr_data_tests; pub struct LidarrData<'a> { + pub add_artist_modal: Option, pub add_artist_search: Option, pub add_import_list_exclusion: bool, pub add_searched_artists: Option>, @@ -92,6 +93,7 @@ impl LidarrData<'_> { impl<'a> Default for LidarrData<'a> { fn default() -> LidarrData<'a> { LidarrData { + add_artist_modal: None, add_artist_search: None, add_import_list_exclusion: false, add_searched_artists: None, @@ -122,6 +124,25 @@ impl<'a> Default for LidarrData<'a> { #[cfg(test)] impl LidarrData<'_> { pub fn test_default_fully_populated() -> Self { + let mut add_artist_modal = AddArtistModal { + tags: "usenet, testing".into(), + ..AddArtistModal::default() + }; + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_artist_modal + .metadata_profile_list + .set_items(vec![metadata_profile().name]); + add_artist_modal + .quality_profile_list + .set_items(vec![quality_profile().name]); + add_artist_modal + .root_folder_list + .set_items(vec![root_folder()]); let mut edit_artist_modal = EditArtistModal { monitored: Some(true), path: "/nfs/music".into(), @@ -144,6 +165,7 @@ impl LidarrData<'_> { quality_profile_map: quality_profile_map(), metadata_profile_map: metadata_profile_map(), edit_artist_modal: Some(edit_artist_modal), + add_artist_modal: Some(add_artist_modal), tags_map: tags_map(), ..LidarrData::default() }; @@ -172,9 +194,18 @@ pub enum ActiveLidarrBlock { #[default] Artists, ArtistsSortPrompt, + AddArtistAlreadyInLibrary, + AddArtistConfirmPrompt, AddArtistEmptySearchResults, + AddArtistPrompt, AddArtistSearchInput, AddArtistSearchResults, + AddArtistSelectMetadataProfile, + AddArtistSelectMonitor, + AddArtistSelectMonitorNewItems, + AddArtistSelectQualityProfile, + AddArtistSelectRootFolder, + AddArtistTagsInput, DeleteArtistPrompt, DeleteArtistConfirmPrompt, DeleteArtistToggleDeleteFile, @@ -204,10 +235,29 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [ ActiveLidarrBlock::UpdateAllArtistsPrompt, ]; -pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 3] = [ +pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 12] = [ + ActiveLidarrBlock::AddArtistAlreadyInLibrary, + ActiveLidarrBlock::AddArtistConfirmPrompt, ActiveLidarrBlock::AddArtistEmptySearchResults, + ActiveLidarrBlock::AddArtistPrompt, ActiveLidarrBlock::AddArtistSearchInput, ActiveLidarrBlock::AddArtistSearchResults, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + ActiveLidarrBlock::AddArtistSelectMonitor, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + ActiveLidarrBlock::AddArtistSelectRootFolder, + ActiveLidarrBlock::AddArtistTagsInput, +]; + +pub const ADD_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::AddArtistSelectRootFolder], + &[ActiveLidarrBlock::AddArtistSelectMonitor], + &[ActiveLidarrBlock::AddArtistSelectMonitorNewItems], + &[ActiveLidarrBlock::AddArtistSelectQualityProfile], + &[ActiveLidarrBlock::AddArtistSelectMetadataProfile], + &[ActiveLidarrBlock::AddArtistTagsInput], + &[ActiveLidarrBlock::AddArtistConfirmPrompt], ]; pub static DELETE_ARTIST_BLOCKS: [ActiveLidarrBlock; 4] = [ diff --git a/src/models/servarr_data/lidarr/lidarr_data_tests.rs b/src/models/servarr_data/lidarr/lidarr_data_tests.rs index 0bf3b3a..ada2a60 100644 --- a/src/models/servarr_data/lidarr/lidarr_data_tests.rs +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -2,8 +2,8 @@ mod tests { use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES; use crate::models::servarr_data::lidarr::lidarr_data::{ - ADD_ARTIST_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_BLOCKS, - EDIT_ARTIST_SELECTION_BLOCKS, + ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, + DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, }; use crate::models::{ BlockSelectionState, Route, @@ -155,10 +155,54 @@ mod tests { #[test] fn test_add_artist_blocks_contents() { - assert_eq!(ADD_ARTIST_BLOCKS.len(), 3); + assert_eq!(ADD_ARTIST_BLOCKS.len(), 12); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistAlreadyInLibrary)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistConfirmPrompt)); assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistEmptySearchResults)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistPrompt)); assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchInput)); assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchResults)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMetadataProfile)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMonitor)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMonitorNewItems)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectQualityProfile)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectRootFolder)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistTagsInput)); + } + + #[test] + fn test_add_artist_selection_blocks_ordering() { + let mut add_artist_block_iter = ADD_ARTIST_SELECTION_BLOCKS.iter(); + + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectRootFolder] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectMonitor] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectMonitorNewItems] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectQualityProfile] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectMetadataProfile] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistTagsInput] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistConfirmPrompt] + ); + assert_none!(add_artist_block_iter.next()); } #[test] diff --git a/src/models/servarr_data/lidarr/modals.rs b/src/models/servarr_data/lidarr/modals.rs index b769d65..6374dc8 100644 --- a/src/models/servarr_data/lidarr/modals.rs +++ b/src/models/servarr_data/lidarr/modals.rs @@ -2,13 +2,50 @@ use strum::IntoEnumIterator; use super::lidarr_data::LidarrData; use crate::models::{ - HorizontallyScrollableText, lidarr_models::NewItemMonitorType, stateful_list::StatefulList, + HorizontallyScrollableText, + lidarr_models::{MonitorType, NewItemMonitorType}, + servarr_models::RootFolder, + stateful_list::StatefulList, }; #[cfg(test)] #[path = "modals_tests.rs"] mod modals_tests; +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct AddArtistModal { + pub root_folder_list: StatefulList, + pub monitor_list: StatefulList, + pub monitor_new_items_list: StatefulList, + pub quality_profile_list: StatefulList, + pub metadata_profile_list: StatefulList, + pub tags: HorizontallyScrollableText, +} + +impl From<&LidarrData<'_>> for AddArtistModal { + fn from(lidarr_data: &LidarrData<'_>) -> AddArtistModal { + let mut add_artist_modal = AddArtistModal::default(); + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_artist_modal + .quality_profile_list + .set_items(lidarr_data.sorted_quality_profile_names()); + add_artist_modal + .metadata_profile_list + .set_items(lidarr_data.sorted_metadata_profile_names()); + add_artist_modal + .root_folder_list + .set_items(lidarr_data.root_folders.items.to_vec()); + + add_artist_modal + } +} + #[derive(Default)] #[cfg_attr(test, derive(Debug))] pub struct EditArtistModal { diff --git a/src/models/servarr_data/lidarr/modals_tests.rs b/src/models/servarr_data/lidarr/modals_tests.rs index ae8b512..5977faf 100644 --- a/src/models/servarr_data/lidarr/modals_tests.rs +++ b/src/models/servarr_data/lidarr/modals_tests.rs @@ -3,9 +3,66 @@ mod tests { use bimap::BiMap; use pretty_assertions::{assert_eq, assert_str_eq}; - use crate::models::lidarr_models::{Artist, NewItemMonitorType}; + use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType}; use crate::models::servarr_data::lidarr::lidarr_data::LidarrData; - use crate::models::servarr_data::lidarr::modals::EditArtistModal; + use crate::models::servarr_data::lidarr::modals::{AddArtistModal, EditArtistModal}; + use crate::models::servarr_models::RootFolder; + + #[test] + fn test_add_artist_modal_from_lidarr_data() { + let mut lidarr_data = LidarrData { + quality_profile_map: BiMap::from_iter([ + (2i64, "Lossless".to_owned()), + (1i64, "Standard".to_owned()), + ]), + metadata_profile_map: BiMap::from_iter([ + (2i64, "None".to_owned()), + (1i64, "Standard".to_owned()), + ]), + ..LidarrData::default() + }; + let root_folder_1 = RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }; + lidarr_data.root_folders.set_items(vec![ + root_folder_1.clone(), + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + + let add_artist_modal = AddArtistModal::from(&lidarr_data); + + assert_eq!( + *add_artist_modal.monitor_list.current_selection(), + MonitorType::default() + ); + assert_eq!( + *add_artist_modal.monitor_new_items_list.current_selection(), + NewItemMonitorType::default() + ); + assert_str_eq!( + add_artist_modal.quality_profile_list.current_selection(), + "Standard" + ); + assert_str_eq!( + add_artist_modal.metadata_profile_list.current_selection(), + "Standard" + ); + assert_eq!( + add_artist_modal.root_folder_list.current_selection(), + &root_folder_1 + ); + assert_is_empty!(add_artist_modal.tags.text); + } #[test] fn test_edit_artist_modal_from_lidarr_data() { diff --git a/src/network/lidarr_network/library/lidarr_library_network_tests.rs b/src/network/lidarr_network/library/lidarr_library_network_tests.rs index ec64c38..781ae1c 100644 --- a/src/network/lidarr_network/library/lidarr_library_network_tests.rs +++ b/src/network/lidarr_network/library/lidarr_library_network_tests.rs @@ -1,8 +1,8 @@ #[cfg(test)] mod tests { use crate::models::lidarr_models::{ - AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams, LidarrSerdeable, - NewItemMonitorType, + AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteArtistParams, + EditArtistParams, LidarrSerdeable, MonitorType, NewItemMonitorType, }; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::network::NetworkResource; @@ -439,4 +439,105 @@ mod tests { assert_some!(&app.data.lidarr_data.add_searched_artists); assert_is_empty!(app.data.lidarr_data.add_searched_artists.as_ref().unwrap()); } + + #[tokio::test] + async fn test_handle_add_artist_event() { + let add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_owned(), + artist_name: "Test Artist".to_owned(), + monitored: true, + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddArtistOptions { + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + search_for_missing_albums: true, + }, + }; + let expected_body = json!({ + "foreignArtistId": "test-foreign-id", + "artistName": "Test Artist", + "monitored": true, + "rootFolderPath": "/music", + "qualityProfileId": 1, + "metadataProfileId": 1, + "tags": [1, 2], + "addOptions": { + "monitor": "all", + "monitorNewItems": "all", + "searchForMissingAlbums": true + } + }); + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(expected_body) + .returns(json!({"id": 1})) + .build_for(LidarrEvent::AddArtist(AddArtistBody::default())) + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::AddArtist(add_artist_body)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_add_artist_event_does_not_overwrite_tags_vec_when_tag_input_string_is_none() + { + let add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_owned(), + artist_name: "Test Artist".to_owned(), + monitored: true, + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: vec![1, 2], + tag_input_string: None, + add_options: AddArtistOptions { + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + search_for_missing_albums: true, + }, + }; + let expected_body = json!({ + "foreignArtistId": "test-foreign-id", + "artistName": "Test Artist", + "monitored": true, + "rootFolderPath": "/music", + "qualityProfileId": 1, + "metadataProfileId": 1, + "tags": [1, 2], + "addOptions": { + "monitor": "all", + "monitorNewItems": "all", + "searchForMissingAlbums": true + } + }); + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(expected_body) + .returns(json!({"id": 1})) + .build_for(LidarrEvent::AddArtist(add_artist_body.clone())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::AddArtist(add_artist_body)) + .await + .is_ok() + ); + + mock.assert_async().await; + } } diff --git a/src/network/lidarr_network/library/mod.rs b/src/network/lidarr_network/library/mod.rs index 0016d08..4a89cd9 100644 --- a/src/network/lidarr_network/library/mod.rs +++ b/src/network/lidarr_network/library/mod.rs @@ -4,7 +4,7 @@ use serde_json::{Value, json}; use crate::models::Route; use crate::models::lidarr_models::{ - AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams, + AddArtistBody, AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams, }; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_models::CommandBody; @@ -213,6 +213,32 @@ impl Network<'_, '_> { result } + pub(in crate::network::lidarr_network) async fn add_artist( + &mut self, + mut add_artist_body: AddArtistBody, + ) -> Result { + info!("Adding Lidarr artist: {}", add_artist_body.artist_name); + if let Some(tag_input_str) = add_artist_body.tag_input_string.as_ref() { + let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await; + add_artist_body.tags = tag_ids_vec; + } + let event = LidarrEvent::AddArtist(AddArtistBody::default()); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + Some(add_artist_body), + None, + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + pub(in crate::network::lidarr_network) async fn edit_artist( &mut self, mut edit_artist_params: EditArtistParams, diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index 3a2dee4..35f66a4 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use crate::app::App; - use crate::models::lidarr_models::{LidarrSerdeable, MetadataProfile}; + use crate::models::lidarr_models::{AddArtistBody, LidarrSerdeable, MetadataProfile}; use crate::models::servarr_data::lidarr::modals::EditArtistModal; use crate::models::servarr_models::{QualityProfile, Tag}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; @@ -18,6 +18,7 @@ mod tests { #[values( LidarrEvent::GetArtistDetails(0), LidarrEvent::ListArtists, + LidarrEvent::AddArtist(AddArtistBody::default()), LidarrEvent::ToggleArtistMonitoring(0) )] event: LidarrEvent, diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index 3406cb8..de9576e 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -3,7 +3,7 @@ use log::info; use super::{NetworkEvent, NetworkResource}; use crate::models::lidarr_models::{ - DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile, + AddArtistBody, DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile, }; use crate::models::servarr_models::{QualityProfile, Tag}; use crate::network::{Network, RequestMethod}; @@ -23,6 +23,7 @@ pub mod lidarr_network_test_utils; #[derive(Debug, Eq, PartialEq, Clone)] pub enum LidarrEvent { + AddArtist(AddArtistBody), AddTag(String), DeleteArtist(DeleteArtistParams), DeleteTag(i64), @@ -52,6 +53,7 @@ impl NetworkResource for LidarrEvent { | LidarrEvent::EditArtist(_) | LidarrEvent::GetArtistDetails(_) | LidarrEvent::ListArtists + | LidarrEvent::AddArtist(_) | LidarrEvent::ToggleArtistMonitoring(_) => "/artist", LidarrEvent::GetDiskSpace => "/diskspace", LidarrEvent::GetDownloads(_) => "/queue", @@ -132,6 +134,7 @@ impl Network<'_, '_> { .map(LidarrSerdeable::from), LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from), LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from), + LidarrEvent::AddArtist(body) => self.add_artist(body).await.map(LidarrSerdeable::from), } } diff --git a/src/ui/lidarr_ui/library/add_artist_ui.rs b/src/ui/lidarr_ui/library/add_artist_ui.rs index e10c891..bb9ab21 100644 --- a/src/ui/lidarr_ui/library/add_artist_ui.rs +++ b/src/ui/lidarr_ui/library/add_artist_ui.rs @@ -2,18 +2,22 @@ use std::sync::atomic::Ordering; use ratatui::Frame; use ratatui::layout::{Constraint, Layout, Rect}; -use ratatui::widgets::{Cell, Row}; +use ratatui::widgets::{Cell, ListItem, Row}; use crate::App; use crate::models::Route; use crate::models::lidarr_models::AddArtistSearchResult; use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock}; +use crate::models::servarr_data::lidarr::modals::AddArtistModal; +use crate::render_selectable_input_box; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block, title_block_centered}; +use crate::ui::widgets::button::Button; use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; use crate::ui::{DrawUi, draw_popup}; #[cfg(test)] @@ -31,7 +35,28 @@ impl DrawUi for AddArtistUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { - draw_popup(f, app, draw_add_artist_search, Size::Large); + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + draw_popup(f, app, draw_add_artist_search, Size::Large); + + match active_lidarr_block { + ActiveLidarrBlock::AddArtistPrompt + | ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder + | ActiveLidarrBlock::AddArtistTagsInput => { + draw_popup(f, app, draw_confirmation_popup, Size::Long); + } + ActiveLidarrBlock::AddArtistAlreadyInLibrary => { + f.render_widget( + Popup::new(Message::new("This artist is already in your library")).size(Size::Message), + f.area(), + ); + } + _ => (), + } + } } } @@ -119,7 +144,15 @@ fn draw_add_artist_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(layout_block().default_color(), results_area); f.render_widget(error_message_popup, f.area()); } - ActiveLidarrBlock::AddArtistSearchResults => { + ActiveLidarrBlock::AddArtistSearchResults + | ActiveLidarrBlock::AddArtistPrompt + | ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder + | ActiveLidarrBlock::AddArtistAlreadyInLibrary + | ActiveLidarrBlock::AddArtistTagsInput => { let search_results_table = ManagarrTable::new( app.data.lidarr_data.add_searched_artists.as_mut(), search_results_row_mapping, @@ -149,3 +182,236 @@ fn draw_add_artist_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { search_box_area, ); } + +fn draw_confirmation_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + match active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_monitor_popup(f, app); + } + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_monitor_new_items_popup(f, app); + } + ActiveLidarrBlock::AddArtistSelectQualityProfile => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_quality_profile_popup(f, app); + } + ActiveLidarrBlock::AddArtistSelectMetadataProfile => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_metadata_profile_popup(f, app); + } + ActiveLidarrBlock::AddArtistSelectRootFolder => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_root_folder_popup(f, app); + } + ActiveLidarrBlock::AddArtistPrompt | ActiveLidarrBlock::AddArtistTagsInput => { + draw_confirmation_prompt(f, app, area) + } + _ => (), + } + } +} + +fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let searched_artist = app + .data + .lidarr_data + .add_searched_artists + .as_ref() + .expect("add_searched_artists must be populated") + .current_selection(); + let artist_name = &searched_artist.artist_name.text; + let artist_disambiguation = searched_artist.disambiguation.clone().unwrap_or_default(); + + let title = if artist_disambiguation.is_empty() { + format!("Add - {artist_name}") + } else { + format!("Add - {artist_name} ({artist_disambiguation})") + }; + let yes_no_value = app.data.lidarr_data.prompt_confirm; + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveLidarrBlock::AddArtistConfirmPrompt; + let AddArtistModal { + monitor_list, + monitor_new_items_list, + quality_profile_list, + metadata_profile_list, + root_folder_list, + tags, + .. + } = app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .expect("add_artist_modal must exist in this context"); + + let selected_monitor = monitor_list.current_selection(); + let selected_monitor_new_items = monitor_new_items_list.current_selection(); + let selected_quality_profile = quality_profile_list.current_selection(); + let selected_metadata_profile = metadata_profile_list.current_selection(); + let selected_root_folder = root_folder_list.current_selection(); + + f.render_widget(title_block_centered(&title), area); + + let [ + _, + root_folder_area, + monitor_area, + monitor_new_items_area, + quality_profile_area, + metadata_profile_area, + tags_area, + _, + buttons_area, + ] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + ]) + .margin(1) + .areas(area); + + let [add_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(buttons_area); + + let root_folder_drop_down_button = Button::default() + .title(&selected_root_folder.path) + .label("Root Folder") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectRootFolder); + let monitor_drop_down_button = Button::default() + .title(selected_monitor.to_display_str()) + .label("Monitor") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectMonitor); + let monitor_new_items_drop_down_button = Button::default() + .title(selected_monitor_new_items.to_display_str()) + .label("Monitor New Items") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectMonitorNewItems); + let quality_profile_drop_down_button = Button::default() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectQualityProfile); + let metadata_profile_drop_down_button = Button::default() + .title(selected_metadata_profile) + .label("Metadata Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectMetadataProfile); + + f.render_widget(root_folder_drop_down_button, root_folder_area); + f.render_widget(monitor_drop_down_button, monitor_area); + f.render_widget(monitor_new_items_drop_down_button, monitor_new_items_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); + f.render_widget(metadata_profile_drop_down_button, metadata_profile_area); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let tags_input_box = InputBox::new(&tags.text) + .offset(tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::AddArtistTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::AddArtistTagsInput); + render_selectable_input_box!(tags_input_box, f, tags_area); + } + + let add_button = Button::default() + .title("Add") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::default() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(add_button, add_area); + f.render_widget(cancel_button, cancel_area); +} + +fn draw_add_artist_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .monitor_list, + |monitor| ListItem::new(monitor.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_artist_select_monitor_new_items_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_new_items_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .monitor_new_items_list, + |monitor_new_items| ListItem::new(monitor_new_items.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_new_items_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_artist_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let quality_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .quality_profile_list, + |quality_profile| ListItem::new(quality_profile.clone()), + ); + let popup = Popup::new(quality_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_artist_select_metadata_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let metadata_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .metadata_profile_list, + |metadata_profile| ListItem::new(metadata_profile.clone()), + ); + let popup = Popup::new(metadata_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_artist_select_root_folder_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let root_folder_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .root_folder_list, + |root_folder| ListItem::new(root_folder.path.to_owned()), + ); + let popup = Popup::new(root_folder_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} diff --git a/src/ui/lidarr_ui/library/add_artist_ui_tests.rs b/src/ui/lidarr_ui/library/add_artist_ui_tests.rs index f756c07..285d575 100644 --- a/src/ui/lidarr_ui/library/add_artist_ui_tests.rs +++ b/src/ui/lidarr_ui/library/add_artist_ui_tests.rs @@ -20,7 +20,8 @@ mod tests { mod snapshot_tests { use super::*; use crate::app::App; - use crate::models::HorizontallyScrollableText; + use crate::models::servarr_data::lidarr::lidarr_data::ADD_ARTIST_SELECTION_BLOCKS; + use crate::models::{BlockSelectionState, HorizontallyScrollableText}; use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; use rstest::rstest; @@ -57,5 +58,67 @@ mod tests { insta::assert_snapshot!(format!("add_artist_ui_{active_lidarr_block}"), output); } + + #[rstest] + #[case(ActiveLidarrBlock::AddArtistPrompt)] + #[case(ActiveLidarrBlock::AddArtistConfirmPrompt)] + #[case(ActiveLidarrBlock::AddArtistSelectMonitor)] + #[case(ActiveLidarrBlock::AddArtistSelectMonitorNewItems)] + #[case(ActiveLidarrBlock::AddArtistSelectQualityProfile)] + #[case(ActiveLidarrBlock::AddArtistSelectMetadataProfile)] + #[case(ActiveLidarrBlock::AddArtistSelectRootFolder)] + #[case(ActiveLidarrBlock::AddArtistTagsInput)] + fn test_add_artist_modal_ui_renders(#[case] active_lidarr_block: ActiveLidarrBlock) { + use crate::models::lidarr_models::{MonitorType, NewItemMonitorType}; + use crate::models::servarr_data::lidarr::modals::AddArtistModal; + use crate::models::servarr_models::RootFolder; + use strum::IntoEnumIterator; + + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + + let mut add_artist_modal = AddArtistModal { + tags: "test".into(), + ..AddArtistModal::default() + }; + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_artist_modal + .quality_profile_list + .set_items(vec!["Any".to_owned()]); + add_artist_modal + .metadata_profile_list + .set_items(vec!["Standard".to_owned()]); + add_artist_modal + .root_folder_list + .set_items(vec![RootFolder { + path: "/nfs/music".to_owned(), + ..RootFolder::default() + }]); + app.data.lidarr_data.add_artist_modal = Some(add_artist_modal); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AddArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("add_artist_modal_{active_lidarr_block}"), output); + } + + #[test] + fn test_add_artist_already_in_library_ui_renders() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AddArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } } } diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_already_in_library_ui_renders.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_already_in_library_ui_renders.snap new file mode 100644 index 0000000..0476b2f --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_already_in_library_ui_renders.snap @@ -0,0 +1,47 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + + ╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮ + │Test Artist │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ ✔ Name Type Status Rating Genres │ + │=> Test Artist Person Continuing 8.4 soundtrack │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────── Error ───────────────╮ │ + │ │This artist is already in your library │ │ + │ │ │ │ + │ ╰───────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistConfirmPrompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistConfirmPrompt.snap new file mode 100644 index 0000000..2b8ab65 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistConfirmPrompt.snap @@ -0,0 +1,14 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + + ╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮ + │Test Artist │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistPrompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistPrompt.snap new file mode 100644 index 0000000..2eaecd9 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Monitor: │All Albums ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Monitor New Items: │All Albums ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Quality Profile: │Any ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Metadata Profile: │Standard ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Tags: │test │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMetadataProfile.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMetadataProfile.snap new file mode 100644 index 0000000..41195bd --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMetadataProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │Standard │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Monito│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Qual│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitor.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitor.snap new file mode 100644 index 0000000..cef8638 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitor.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │All Albums │ ▼ │ │ │ + │ │ │Future Albums │──────────────────────────────╯ │ │ + │ │ │Missing Albums │──────────────────────────────╮ │ │ + │ │ Monito│Existing Albums │ ▼ │ │ │ + │ │ │First Album │──────────────────────────────╯ │ │ + │ │ │Latest Album │──────────────────────────────╮ │ │ + │ │ Qual│None │ ▼ │ │ │ + │ │ │Unknown │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitorNewItems.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitorNewItems.snap new file mode 100644 index 0000000..0cb823c --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitorNewItems.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │All Albums │ ▼ │ │ │ + │ │ │No New Albums │──────────────────────────────╯ │ │ + │ │ │New Albums │──────────────────────────────╮ │ │ + │ │ Monito│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Qual│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectQualityProfile.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectQualityProfile.snap new file mode 100644 index 0000000..2a74c48 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectQualityProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │Any │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Monito│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Qual│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectRootFolder.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectRootFolder.snap new file mode 100644 index 0000000..90a69df --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectRootFolder.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │/nfs/music │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Monito│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Qual│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistTagsInput.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistTagsInput.snap new file mode 100644 index 0000000..2eaecd9 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistTagsInput.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Monitor: │All Albums ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Monitor New Items: │All Albums ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Quality Profile: │Any ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Metadata Profile: │Standard ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Tags: │test │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯