From 3c1634d1e355a21b9b5fe44711c3c24ba168003c Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 7 Jan 2026 10:45:49 -0700 Subject: [PATCH] testing --- src/app/lidarr/lidarr_context_clues.rs | 3 +- src/app/lidarr/lidarr_context_clues_tests.rs | 4 + src/cli/lidarr/edit_command_handler.rs | 146 ++++++ src/cli/lidarr/edit_command_handler_tests.rs | 409 ++++++++++++++++ src/cli/lidarr/mod.rs | 12 + src/handlers/keybinding_handler.rs | 3 +- .../library/delete_artist_handler.rs | 3 +- .../library/edit_artist_handler.rs | 455 ++++++++++++++++++ .../library/edit_artist_handler_tests.rs | 215 +++++++++ .../library/library_handler_tests.rs | 13 +- src/handlers/lidarr_handlers/library/mod.rs | 37 +- .../lidarr_handlers/lidarr_handler_tests.rs | 38 +- src/handlers/lidarr_handlers/mod.rs | 10 +- src/handlers/radarr_handlers/blocklist/mod.rs | 3 +- .../collections/collection_details_handler.rs | 4 +- .../collections/edit_collection_handler.rs | 4 +- .../radarr_handlers/collections/mod.rs | 4 +- src/handlers/radarr_handlers/downloads/mod.rs | 3 +- .../indexers/edit_indexer_handler.rs | 3 +- .../indexers/edit_indexer_settings_handler.rs | 3 +- src/handlers/radarr_handlers/indexers/mod.rs | 4 +- .../indexers/test_all_indexers_handler.rs | 3 +- .../library/add_movie_handler.rs | 4 +- .../library/delete_movie_handler.rs | 3 +- .../library/edit_movie_handler.rs | 4 +- .../library/movie_details_handler.rs | 4 +- src/handlers/radarr_handlers/mod.rs | 3 +- .../radarr_handlers/root_folders/mod.rs | 4 +- src/handlers/radarr_handlers/system/mod.rs | 4 +- .../system/system_details_handler.rs | 4 +- src/handlers/sonarr_handlers/blocklist/mod.rs | 3 +- src/handlers/sonarr_handlers/downloads/mod.rs | 3 +- src/handlers/sonarr_handlers/history/mod.rs | 3 +- .../indexers/edit_indexer_handler.rs | 3 +- .../indexers/edit_indexer_settings_handler.rs | 3 +- src/handlers/sonarr_handlers/indexers/mod.rs | 4 +- .../indexers/test_all_indexers_handler.rs | 3 +- .../library/add_series_handler.rs | 4 +- .../library/delete_series_handler.rs | 3 +- .../library/edit_series_handler.rs | 4 +- .../library/episode_details_handler.rs | 3 +- src/handlers/sonarr_handlers/library/mod.rs | 3 +- .../library/season_details_handler.rs | 3 +- .../library/series_details_handler.rs | 4 +- src/handlers/sonarr_handlers/mod.rs | 4 +- .../sonarr_handlers/root_folders/mod.rs | 4 +- src/handlers/sonarr_handlers/system/mod.rs | 4 +- .../system/system_details_handler.rs | 4 +- src/handlers/table_handler_tests.rs | 3 +- src/models/lidarr_models.rs | 17 + src/models/servarr_data/lidarr/lidarr_data.rs | 103 +++- .../servarr_data/lidarr/lidarr_data_tests.rs | 78 ++- src/models/servarr_data/lidarr/mod.rs | 1 + src/models/servarr_data/lidarr/modals.rs | 78 +++ .../servarr_data/lidarr/modals_tests.rs | 48 ++ .../servarr_data/radarr/radarr_data_tests.rs | 32 +- .../servarr_data/sonarr/sonarr_data_tests.rs | 47 +- src/network/lidarr_network/library/mod.rs | 117 ++++- .../lidarr_network_test_utils.rs | 132 +++++ src/network/lidarr_network/mod.rs | 72 ++- .../sonarr_network_test_utils.rs | 1 + src/ui/lidarr_ui/library/edit_artist_ui.rs | 222 +++++++++ .../lidarr_ui/library/edit_artist_ui_tests.rs | 22 + src/ui/lidarr_ui/library/library_ui_tests.rs | 4 +- src/ui/lidarr_ui/library/mod.rs | 7 +- 65 files changed, 2355 insertions(+), 100 deletions(-) create mode 100644 src/cli/lidarr/edit_command_handler.rs create mode 100644 src/cli/lidarr/edit_command_handler_tests.rs create mode 100644 src/handlers/lidarr_handlers/library/edit_artist_handler.rs create mode 100644 src/handlers/lidarr_handlers/library/edit_artist_handler_tests.rs create mode 100644 src/models/servarr_data/lidarr/modals.rs create mode 100644 src/models/servarr_data/lidarr/modals_tests.rs create mode 100644 src/network/lidarr_network/lidarr_network_test_utils.rs create mode 100644 src/ui/lidarr_ui/library/edit_artist_ui.rs create mode 100644 src/ui/lidarr_ui/library/edit_artist_ui_tests.rs diff --git a/src/app/lidarr/lidarr_context_clues.rs b/src/app/lidarr/lidarr_context_clues.rs index 7c696c2..b33782b 100644 --- a/src/app/lidarr/lidarr_context_clues.rs +++ b/src/app/lidarr/lidarr_context_clues.rs @@ -7,12 +7,13 @@ use crate::models::Route; #[path = "lidarr_context_clues_tests.rs"] mod lidarr_context_clues_tests; -pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 8] = [ +pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 9] = [ ( DEFAULT_KEYBINDINGS.toggle_monitoring, DEFAULT_KEYBINDINGS.toggle_monitoring.desc, ), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), diff --git a/src/app/lidarr/lidarr_context_clues_tests.rs b/src/app/lidarr/lidarr_context_clues_tests.rs index b219954..5348871 100644 --- a/src/app/lidarr/lidarr_context_clues_tests.rs +++ b/src/app/lidarr/lidarr_context_clues_tests.rs @@ -24,6 +24,10 @@ mod tests { artists_context_clues_iter.next(), &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc) + ); assert_some_eq_x!( artists_context_clues_iter.next(), &(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc) diff --git a/src/cli/lidarr/edit_command_handler.rs b/src/cli/lidarr/edit_command_handler.rs new file mode 100644 index 0000000..2fc0bd5 --- /dev/null +++ b/src/cli/lidarr/edit_command_handler.rs @@ -0,0 +1,146 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::{ArgAction, ArgGroup, Subcommand}; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command, mutex_flags_or_option}, + models::lidarr_models::{EditArtistParams, NewItemMonitorType}, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +use super::LidarrCommand; + +#[cfg(test)] +#[path = "edit_command_handler_tests.rs"] +mod edit_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrEditCommand { + #[command( + about = "Edit preferences for the specified artist", + group( + ArgGroup::new("edit_artist") + .args([ + "enable_monitoring", + "disable_monitoring", + "monitor_new_items", + "quality_profile_id", + "metadata_profile_id", + "root_folder_path", + "tag", + "clear_tags" + ]).required(true) + .multiple(true)) + )] + Artist { + #[arg( + long, + help = "The ID of the artist whose settings you want to edit", + required = true + )] + artist_id: i64, + #[arg( + long, + help = "Enable monitoring of this artist in Lidarr so Lidarr will automatically download releases from this artist if they are available", + conflicts_with = "disable_monitoring" + )] + enable_monitoring: bool, + #[arg( + long, + help = "Disable monitoring of this artist so Lidarr does not automatically download releases from this artist if they are available", + conflicts_with = "enable_monitoring" + )] + disable_monitoring: bool, + #[arg( + long, + help = "How Lidarr should monitor new albums from this artist", + value_enum + )] + monitor_new_items: Option, + #[arg(long, help = "The ID of the quality profile to use for this artist")] + quality_profile_id: Option, + #[arg(long, help = "The ID of the metadata profile to use for this artist")] + metadata_profile_id: Option, + #[arg( + long, + help = "The root folder path where all artist data and metadata should live" + )] + root_folder_path: Option, + #[arg( + long, + help = "Tag IDs to tag this artist with", + value_parser, + action = ArgAction::Append, + conflicts_with = "clear_tags" + )] + tag: Option>, + #[arg(long, help = "Clear all tags on this artist", conflicts_with = "tag")] + clear_tags: bool, + }, +} + +impl From for Command { + fn from(value: LidarrEditCommand) -> Self { + Command::Lidarr(LidarrCommand::Edit(value)) + } +} + +pub(super) struct LidarrEditCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrEditCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + command: LidarrEditCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrEditCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrEditCommand::Artist { + artist_id, + enable_monitoring, + disable_monitoring, + monitor_new_items, + quality_profile_id, + metadata_profile_id, + root_folder_path, + tag, + clear_tags, + } => { + let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring); + let edit_artist_params = EditArtistParams { + artist_id, + monitored: monitored_value, + monitor_new_items, + quality_profile_id, + metadata_profile_id, + root_folder_path, + tags: tag, + tag_input_string: None, + clear_tags, + }; + + self + .network + .handle_network_event(LidarrEvent::EditArtist(edit_artist_params).into()) + .await?; + "Artist Updated".to_owned() + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/edit_command_handler_tests.rs b/src/cli/lidarr/edit_command_handler_tests.rs new file mode 100644 index 0000000..bed34f0 --- /dev/null +++ b/src/cli/lidarr/edit_command_handler_tests.rs @@ -0,0 +1,409 @@ +#[cfg(test)] +mod tests { + use crate::cli::{ + Command, + lidarr::{LidarrCommand, edit_command_handler::LidarrEditCommand}, + }; + + #[test] + fn test_lidarr_edit_command_from() { + let command = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: false, + monitor_new_items: None, + quality_profile_id: None, + metadata_profile_id: None, + root_folder_path: None, + tag: None, + clear_tags: false, + }; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(LidarrCommand::Edit(command))); + } + + mod cli { + use crate::{Cli, models::lidarr_models::NewItemMonitorType}; + + use super::*; + use clap::{CommandFactory, Parser, error::ErrorKind}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[test] + fn test_edit_artist_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "artist"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_artist_with_artist_id_still_requires_arguments() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_artist_monitoring_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--enable-monitoring", + "--disable-monitoring", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_artist_tag_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--tag", + "1", + "--clear-tags", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[rstest] + fn test_edit_artist_assert_argument_flags_require_args( + #[values( + "--monitor-new-items", + "--quality-profile-id", + "--metadata-profile-id", + "--root-folder-path", + "--tag" + )] + flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + flag, + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_artist_monitor_new_items_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--monitor-new-items", + "test", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_artist_only_requires_at_least_one_argument_plus_artist_id() { + let expected_args = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: false, + monitor_new_items: None, + quality_profile_id: None, + metadata_profile_id: None, + root_folder_path: Some("/nfs/test".to_owned()), + tag: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--root-folder-path", + "/nfs/test", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_artist_tag_argument_is_repeatable() { + let expected_args = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: false, + monitor_new_items: None, + quality_profile_id: None, + metadata_profile_id: None, + root_folder_path: None, + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--tag", + "1", + "--tag", + "2", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_artist_all_arguments_defined() { + let expected_args = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: true, + disable_monitoring: false, + monitor_new_items: Some(NewItemMonitorType::New), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--enable-monitoring", + "--monitor-new-items", + "new", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + "--root-folder-path", + "/nfs/test", + "--tag", + "1", + "--tag", + "2", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{ + app::App, + cli::{ + CliCommandHandler, + lidarr::edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler}, + }, + models::{ + Serdeable, + lidarr_models::{EditArtistParams, LidarrSerdeable, NewItemMonitorType}, + }, + network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, + }; + + #[tokio::test] + async fn test_handle_edit_artist_command() { + let expected_edit_artist_params = EditArtistParams { + artist_id: 1, + monitored: Some(true), + monitor_new_items: Some(NewItemMonitorType::New), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + tag_input_string: None, + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditArtist(expected_edit_artist_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_artist_command = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: true, + disable_monitoring: false, + monitor_new_items: Some(NewItemMonitorType::New), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_edit_artist_command_handles_disable_monitoring_flag_properly() { + let expected_edit_artist_params = EditArtistParams { + artist_id: 1, + monitored: Some(false), + monitor_new_items: Some(NewItemMonitorType::None), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + tag_input_string: None, + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditArtist(expected_edit_artist_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_artist_command = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: true, + monitor_new_items: Some(NewItemMonitorType::None), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_edit_artist_command_no_monitoring_boolean_flags_returns_none_value() { + let expected_edit_artist_params = EditArtistParams { + artist_id: 1, + monitored: None, + monitor_new_items: Some(NewItemMonitorType::All), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + tag_input_string: None, + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditArtist(expected_edit_artist_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_artist_command = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: false, + monitor_new_items: Some(NewItemMonitorType::All), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/mod.rs b/src/cli/lidarr/mod.rs index fcc37c8..a7a4fcc 100644 --- a/src/cli/lidarr/mod.rs +++ b/src/cli/lidarr/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Result; use clap::{Subcommand, arg}; use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler}; +use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler}; use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler}; use list_command_handler::{LidarrListCommand, LidarrListCommandHandler}; use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler}; @@ -14,6 +15,7 @@ use crate::{app::App, network::NetworkTrait}; use super::{CliCommandHandler, Command}; mod delete_command_handler; +mod edit_command_handler; mod get_command_handler; mod list_command_handler; mod refresh_command_handler; @@ -29,6 +31,11 @@ pub enum LidarrCommand { about = "Commands to delete resources from your Lidarr instance" )] Delete(LidarrDeleteCommand), + #[command( + subcommand, + about = "Commands to edit resources in your Lidarr instance" + )] + Edit(LidarrEditCommand), #[command( subcommand, about = "Commands to fetch details of the resources in your Lidarr instance" @@ -89,6 +96,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, ' .handle() .await? } + LidarrCommand::Edit(edit_command) => { + LidarrEditCommandHandler::with(self.app, edit_command, self.network) + .handle() + .await? + } LidarrCommand::Get(get_command) => { LidarrGetCommandHandler::with(self.app, get_command, self.network) .handle() diff --git a/src/handlers/keybinding_handler.rs b/src/handlers/keybinding_handler.rs index b9560a0..6965b1e 100644 --- a/src/handlers/keybinding_handler.rs +++ b/src/handlers/keybinding_handler.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; use crate::models::servarr_data::ActiveKeybindingBlock; #[cfg(test)] @@ -75,7 +76,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandle self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/lidarr_handlers/library/delete_artist_handler.rs b/src/handlers/lidarr_handlers/library/delete_artist_handler.rs index 48c8251..31affd7 100644 --- a/src/handlers/lidarr_handlers/library/delete_artist_handler.rs +++ b/src/handlers/lidarr_handlers/library/delete_artist_handler.rs @@ -7,6 +7,7 @@ use crate::{ matches_key, models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS}, }; +use crate::models::Route; #[cfg(test)] #[path = "delete_artist_handler_tests.rs"] @@ -143,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler< self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/lidarr_handlers/library/edit_artist_handler.rs b/src/handlers/lidarr_handlers/library/edit_artist_handler.rs new file mode 100644 index 0000000..3b62fae --- /dev/null +++ b/src/handlers/lidarr_handlers/library/edit_artist_handler.rs @@ -0,0 +1,455 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::{Route, Scrollable}; +use crate::models::lidarr_models::EditArtistParams; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS}; +use crate::models::servarr_data::lidarr::modals::EditArtistModal; +use crate::network::lidarr_network::LidarrEvent; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; + +#[cfg(test)] +#[path = "edit_artist_handler_tests.rs"] +mod edit_artist_handler_tests; + +pub(in crate::handlers::lidarr_handlers) struct EditArtistHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl EditArtistHandler<'_, '_> { + fn build_edit_artist_params(&mut self) -> EditArtistParams { + let edit_artist_modal = self + .app + .data + .lidarr_data + .edit_artist_modal + .take() + .expect("EditArtistModal is None"); + let artist_id = self.app.data.lidarr_data.artists.current_selection().id; + let tags = edit_artist_modal.tags.text; + + let EditArtistModal { + monitored, + path, + monitor_list, + quality_profile_list, + metadata_profile_list, + .. + } = edit_artist_modal; + 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(); + + EditArtistParams { + artist_id, + monitored, + monitor_new_items: Some(*monitor_list.current_selection()), + quality_profile_id: Some(quality_profile_id), + metadata_profile_id: Some(metadata_profile_id), + root_folder_path: Some(path.text), + tag_input_string: Some(tags), + ..EditArtistParams::default() + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditArtistHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + EDIT_ARTIST_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> EditArtistHandler<'a, 'b> { + EditArtistHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.lidarr_data.edit_artist_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_up(), + ActiveLidarrBlock::EditArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_up(), + ActiveLidarrBlock::EditArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_up(), + ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_down(), + ActiveLidarrBlock::EditArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_down(), + ActiveLidarrBlock::EditArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_down(), + ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_top(), + ActiveLidarrBlock::EditArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_top(), + ActiveLidarrBlock::EditArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_top(), + ActiveLidarrBlock::EditArtistPathInput => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .path + .scroll_home(), + ActiveLidarrBlock::EditArtistTagsInput => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + .scroll_home(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_bottom(), + ActiveLidarrBlock::EditArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::EditArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::EditArtistPathInput => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .path + .reset_offset(), + ActiveLidarrBlock::EditArtistTagsInput => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + .reset_offset(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistPrompt => handle_prompt_toggle(self.app, self.key), + ActiveLidarrBlock::EditArtistPathInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveLidarrBlock::EditArtistTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistPrompt => { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::EditArtistConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::EditArtist(self.build_edit_artist_params())); + self.app.should_refresh = true; + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::EditArtistSelectMonitorNewItems + | ActiveLidarrBlock::EditArtistSelectQualityProfile + | ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.push_navigation_stack( + ( + self.app.data.lidarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ), + ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => { + self.app.push_navigation_stack( + ( + self.app.data.lidarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ); + self.app.ignore_special_keys_for_textbox_input = true; + } + ActiveLidarrBlock::EditArtistToggleMonitored => { + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitored = Some( + !self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitored + .unwrap_or_default(), + ) + } + _ => (), + } + } + ActiveLidarrBlock::EditArtistSelectMonitorNewItems + | ActiveLidarrBlock::EditArtistSelectQualityProfile + | ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(), + ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistTagsInput | ActiveLidarrBlock::EditArtistPathInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::EditArtistPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.edit_artist_modal = None; + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::EditArtistSelectMonitorNewItems + | ActiveLidarrBlock::EditArtistSelectQualityProfile + | ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistPathInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveLidarrBlock::EditArtistTagsInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + ) + } + ActiveLidarrBlock::EditArtistPrompt => { + if self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::EditArtistConfirmPrompt + && matches_key!(confirm, key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::EditArtist(self.build_edit_artist_params())); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/library/edit_artist_handler_tests.rs b/src/handlers/lidarr_handlers/library/edit_artist_handler_tests.rs new file mode 100644 index 0000000..1f1ed9b --- /dev/null +++ b/src/handlers/lidarr_handlers/library/edit_artist_handler_tests.rs @@ -0,0 +1,215 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::edit_artist_handler::EditArtistHandler; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS}; + use crate::models::servarr_data::lidarr::modals::EditArtistModal; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_ARTIST_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_edit_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditArtistToggleMonitored + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditArtistSelectQualityProfile + ); + } + } + + #[rstest] + fn test_edit_artist_prompt_scroll_no_op_when_not_ready(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditArtistSelectMonitorNewItems + ); + } + } + + mod test_handle_left_right_action { + 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.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + + EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_ARTIST_SELECTION_BLOCKS; + + use super::*; + use crate::assert_navigation_popped; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_artist_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + // Navigate to the confirm prompt (last selection block) + for _ in 0..EDIT_ARTIST_SELECTION_BLOCKS.len() - 1 { + app.data.lidarr_data.selected_block.down(); + } + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm_action.is_none()); + assert_navigation_popped!(&app, ActiveLidarrBlock::Artists.into()); + } + } + + mod test_handle_esc { + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_edit_artist_prompt_esc() { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + EditArtistHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + assert_navigation_popped!(&app, ActiveLidarrBlock::Artists.into()); + assert!(app.data.lidarr_data.edit_artist_modal.is_none()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_edit_artist_select_blocks_esc() { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistSelectQualityProfile.into()); + + EditArtistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_navigation_popped!(&app, ActiveLidarrBlock::EditArtistPrompt.into()); + } + } + + #[test] + fn test_edit_artist_handler_accepts() { + let mut edit_artist_handler_blocks = Vec::new(); + for block in ActiveLidarrBlock::iter() { + if EditArtistHandler::accepts(block) { + edit_artist_handler_blocks.push(block); + } + } + + assert_eq!(edit_artist_handler_blocks, EDIT_ARTIST_BLOCKS.to_vec()); + } + + #[test] + fn test_edit_artist_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + + let handler = EditArtistHandler::new( + Key::Esc, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_artist_handler_is_not_ready_when_edit_artist_modal_is_none() { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = None; + + let handler = EditArtistHandler::new( + Key::Esc, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_artist_handler_is_ready_when_not_loading_and_modal_is_some() { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + + let handler = EditArtistHandler::new( + Key::Esc, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/library/library_handler_tests.rs b/src/handlers/lidarr_handlers/library/library_handler_tests.rs index a987d04..4a7d94e 100644 --- a/src/handlers/lidarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/lidarr_handlers/library/library_handler_tests.rs @@ -11,19 +11,24 @@ mod tests { use crate::handlers::KeyEventHandler; use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options}; use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus}; - use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS}; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS}; use crate::network::lidarr_network::LidarrEvent; use crate::{assert_modal_absent, assert_navigation_popped, assert_navigation_pushed}; #[test] fn test_library_handler_accepts() { - for lidarr_block in ActiveLidarrBlock::iter() { - if LIBRARY_BLOCKS.contains(&lidarr_block) { + let mut library_handler_blocks = Vec::new(); + library_handler_blocks.extend(LIBRARY_BLOCKS); + library_handler_blocks.extend(DELETE_ARTIST_BLOCKS); + library_handler_blocks.extend(EDIT_ARTIST_BLOCKS); + + ActiveLidarrBlock::iter().for_each(|lidarr_block| { + if library_handler_blocks.contains(&lidarr_block) { assert!(LibraryHandler::accepts(lidarr_block)); } else { assert!(!LibraryHandler::accepts(lidarr_block)); } - } + }); } #[test] diff --git a/src/handlers/lidarr_handlers/library/mod.rs b/src/handlers/lidarr_handlers/library/mod.rs index d9e4482..114442d 100644 --- a/src/handlers/lidarr_handlers/library/mod.rs +++ b/src/handlers/lidarr_handlers/library/mod.rs @@ -7,8 +7,10 @@ use crate::{ BlockSelectionState, lidarr_models::Artist, servarr_data::lidarr::lidarr_data::{ - ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS, + ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, + LIBRARY_BLOCKS, }, + servarr_data::lidarr::modals::EditArtistModal, stateful_table::SortOption, }, network::lidarr_network::LidarrEvent, @@ -18,8 +20,11 @@ use super::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; mod delete_artist_handler; +mod edit_artist_handler; pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler; +pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler; +use crate::models::Route; #[cfg(test)] #[path = "library_handler_tests.rs"] @@ -29,7 +34,7 @@ pub(super) struct LibraryHandler<'a, 'b> { key: Key, app: &'a mut App<'b>, active_lidarr_block: ActiveLidarrBlock, - _context: Option, + context: Option, } impl LibraryHandler<'_, '_> { @@ -55,12 +60,23 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, ' |app| &mut app.data.lidarr_data.artists, artists_table_handling_config, ) { - self.handle_key_event(); + match self.active_lidarr_block { + _ if DeleteArtistHandler::accepts(self.active_lidarr_block) => { + DeleteArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ if EditArtistHandler::accepts(self.active_lidarr_block) => { + EditArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } + _ => self.handle_key_event(), + } } } fn accepts(active_block: ActiveLidarrBlock) -> bool { - LIBRARY_BLOCKS.contains(&active_block) + DeleteArtistHandler::accepts(active_block) + || EditArtistHandler::accepts(active_block) + || LIBRARY_BLOCKS.contains(&active_block) } fn ignore_special_keys(&self) -> bool { @@ -77,7 +93,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, ' key, app, active_lidarr_block: active_block, - _context: context, + context, } } @@ -151,6 +167,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, ' .app .pop_and_push_navigation_stack(self.active_lidarr_block.into()); } + _ if matches_key!(edit, key) => { + self.app.data.lidarr_data.edit_artist_modal = + Some((&self.app.data.lidarr_data).into()); + self + .app + .push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + } _ if matches_key!(update, key) => { self .app @@ -177,7 +202,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, ' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs index 34af245..c180d35 100644 --- a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs +++ b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs @@ -1,11 +1,47 @@ #[cfg(test)] mod tests { + use rstest::rstest; use strum::IntoEnumIterator; - + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::handlers::KeyEventHandler; use crate::handlers::lidarr_handlers::LidarrHandler; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + #[rstest] + fn test_lidarr_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = LidarrHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_lidarr_handler_is_ready() { + let mut app = App::test_default(); + app.is_loading = true; + + let handler = LidarrHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert!(handler.is_ready()); + } + #[test] fn test_lidarr_handler_accepts() { for lidarr_block in ActiveLidarrBlock::iter() { diff --git a/src/handlers/lidarr_handlers/mod.rs b/src/handlers/lidarr_handlers/mod.rs index 6932737..16ff15b 100644 --- a/src/handlers/lidarr_handlers/mod.rs +++ b/src/handlers/lidarr_handlers/mod.rs @@ -1,9 +1,9 @@ -use library::{DeleteArtistHandler, LibraryHandler}; +use library::LibraryHandler; use crate::{ app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, }; - +use crate::models::Route; use super::KeyEventHandler; mod library; @@ -22,10 +22,6 @@ pub(super) struct LidarrHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b> { fn handle(&mut self) { match self.active_lidarr_block { - _ if DeleteArtistHandler::accepts(self.active_lidarr_block) => { - DeleteArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context) - .handle(); - } _ if LibraryHandler::accepts(self.active_lidarr_block) => { LibraryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); } @@ -85,7 +81,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/blocklist/mod.rs b/src/handlers/radarr_handlers/blocklist/mod.rs index 9cf43a0..44dcbbb 100644 --- a/src/handlers/radarr_handlers/blocklist/mod.rs +++ b/src/handlers/radarr_handlers/blocklist/mod.rs @@ -5,6 +5,7 @@ use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; use crate::models::radarr_models::BlocklistItem; +use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::stateful_table::SortOption; use crate::network::radarr_network::RadarrEvent; @@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/collections/collection_details_handler.rs b/src/handlers/radarr_handlers/collections/collection_details_handler.rs index 2cb628b..5fb64a2 100644 --- a/src/handlers/radarr_handlers/collections/collection_details_handler.rs +++ b/src/handlers/radarr_handlers/collections/collection_details_handler.rs @@ -3,7 +3,7 @@ use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::matches_key; -use crate::models::BlockSelectionState; +use crate::models::{BlockSelectionState, Route}; use crate::models::servarr_data::radarr::radarr_data::{ ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, @@ -148,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs index dff7e63..1f38ce5 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs @@ -1,7 +1,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; -use crate::models::Scrollable; +use crate::models::{Route, Scrollable}; use crate::models::radarr_models::EditCollectionParams; use crate::models::servarr_data::radarr::modals::EditCollectionModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS}; @@ -376,7 +376,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/collections/mod.rs b/src/handlers/radarr_handlers/collections/mod.rs index 69e22d1..bcc2370 100644 --- a/src/handlers/radarr_handlers/collections/mod.rs +++ b/src/handlers/radarr_handlers/collections/mod.rs @@ -6,7 +6,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; -use crate::models::BlockSelectionState; +use crate::models::{BlockSelectionState, Route}; use crate::models::radarr_models::Collection; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, @@ -179,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/downloads/mod.rs b/src/handlers/radarr_handlers/downloads/mod.rs index 3a449d1..53899a5 100644 --- a/src/handlers/radarr_handlers/downloads/mod.rs +++ b/src/handlers/radarr_handlers/downloads/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; use crate::network::radarr_network::RadarrEvent; @@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs index 33a2383..1c224c3 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs @@ -8,6 +8,7 @@ use crate::network::radarr_network::RadarrEvent; use crate::{ handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key, }; +use crate::models::Route; #[cfg(test)] #[path = "edit_indexer_handler_tests.rs"] @@ -527,7 +528,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs index f933187..f237f97 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -9,6 +9,7 @@ use crate::network::radarr_network::RadarrEvent; use crate::{ handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key, }; +use crate::models::Route; #[cfg(test)] #[path = "edit_indexer_settings_handler_tests.rs"] @@ -293,7 +294,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/indexers/mod.rs b/src/handlers/radarr_handlers/indexers/mod.rs index 85f5adc..c37d9f6 100644 --- a/src/handlers/radarr_handlers/indexers/mod.rs +++ b/src/handlers/radarr_handlers/indexers/mod.rs @@ -7,7 +7,7 @@ use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestA use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; -use crate::models::BlockSelectionState; +use crate::models::{BlockSelectionState, Route}; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, @@ -212,7 +212,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs index c8e26d4..a19cbf2 100644 --- a/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs +++ b/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; #[cfg(test)] @@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index e668304..555b93c 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -7,7 +7,7 @@ use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, }; -use crate::models::{BlockSelectionState, Scrollable}; +use crate::models::{BlockSelectionState, Route, Scrollable}; use crate::network::radarr_network::RadarrEvent; use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -558,7 +558,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler.rs b/src/handlers/radarr_handlers/library/delete_movie_handler.rs index 561489f..ab25e4c 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler.rs @@ -3,6 +3,7 @@ use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; use crate::models::radarr_models::DeleteMovieParams; +use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS}; use crate::network::radarr_network::RadarrEvent; @@ -141,7 +142,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler.rs b/src/handlers/radarr_handlers/library/edit_movie_handler.rs index f463d0f..db5e4b7 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler.rs @@ -1,7 +1,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; -use crate::models::Scrollable; +use crate::models::{Route, Scrollable}; use crate::models::radarr_models::EditMovieParams; use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS}; @@ -397,7 +397,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index 9877b79..bf321a1 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -11,7 +11,7 @@ use crate::models::servarr_data::radarr::radarr_data::{ }; use crate::models::servarr_models::Language; use crate::models::stateful_table::SortOption; -use crate::models::{BlockSelectionState, Scrollable}; +use crate::models::{BlockSelectionState, Route, Scrollable}; use crate::network::radarr_network::RadarrEvent; #[cfg(test)] @@ -379,7 +379,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index d8f8075..a785c41 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -8,6 +8,7 @@ use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler; use crate::handlers::radarr_handlers::system::SystemHandler; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::{App, Key, matches_key}; +use crate::models::Route; mod blocklist; mod collections; @@ -112,7 +113,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/root_folders/mod.rs b/src/handlers/radarr_handlers/root_folders/mod.rs index aa2a64f..27a3bd4 100644 --- a/src/handlers/radarr_handlers/root_folders/mod.rs +++ b/src/handlers/radarr_handlers/root_folders/mod.rs @@ -3,7 +3,7 @@ use crate::event::Key; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; -use crate::models::HorizontallyScrollableText; +use crate::models::{HorizontallyScrollableText, Route}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS}; use crate::models::servarr_models::AddRootFolderBody; use crate::network::radarr_network::RadarrEvent; @@ -231,7 +231,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/system/mod.rs b/src/handlers/radarr_handlers/system/mod.rs index 50201b0..bf3cc56 100644 --- a/src/handlers/radarr_handlers/system/mod.rs +++ b/src/handlers/radarr_handlers/system/mod.rs @@ -4,7 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler; use crate::handlers::{KeyEventHandler, handle_clear_errors}; use crate::matches_key; -use crate::models::Scrollable; +use crate::models::{Route, Scrollable}; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; mod system_details_handler; @@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/system/system_details_handler.rs b/src/handlers/radarr_handlers/system/system_details_handler.rs index 7b13d4e..60db4c7 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler.rs @@ -2,7 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; -use crate::models::Scrollable; +use crate::models::{Route, Scrollable}; use crate::models::radarr_models::RadarrTaskName; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::stateful_list::StatefulList; @@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/blocklist/mod.rs b/src/handlers/sonarr_handlers/blocklist/mod.rs index 2989042..c09f829 100644 --- a/src/handlers/sonarr_handlers/blocklist/mod.rs +++ b/src/handlers/sonarr_handlers/blocklist/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS}; use crate::models::sonarr_models::BlocklistItem; use crate::models::stateful_table::SortOption; @@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/downloads/mod.rs b/src/handlers/sonarr_handlers/downloads/mod.rs index 7a9819a..f585388 100644 --- a/src/handlers/sonarr_handlers/downloads/mod.rs +++ b/src/handlers/sonarr_handlers/downloads/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; use crate::network::sonarr_network::SonarrEvent; @@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/history/mod.rs b/src/handlers/sonarr_handlers/history/mod.rs index 049de25..827270f 100644 --- a/src/handlers/sonarr_handlers/history/mod.rs +++ b/src/handlers/sonarr_handlers/history/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; use crate::models::servarr_models::Language; use crate::models::sonarr_models::SonarrHistoryItem; @@ -121,7 +122,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, ' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs index c253008..c6613aa 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs @@ -8,6 +8,7 @@ use crate::network::sonarr_network::SonarrEvent; use crate::{ handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key, }; +use crate::models::Route; #[cfg(test)] #[path = "edit_indexer_handler_tests.rs"] @@ -526,7 +527,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs index 8da2449..22d0ea8 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -7,6 +7,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ use crate::models::sonarr_models::IndexerSettings; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_prompt_left_right_keys, matches_key}; +use crate::models::Route; #[cfg(test)] #[path = "edit_indexer_settings_handler_tests.rs"] @@ -202,7 +203,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/indexers/mod.rs b/src/handlers/sonarr_handlers/indexers/mod.rs index cc33890..3d3cd46 100644 --- a/src/handlers/sonarr_handlers/indexers/mod.rs +++ b/src/handlers/sonarr_handlers/indexers/mod.rs @@ -7,7 +7,7 @@ use crate::handlers::sonarr_handlers::indexers::test_all_indexers_handler::TestA use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; -use crate::models::BlockSelectionState; +use crate::models::{BlockSelectionState, Route}; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, @@ -211,7 +211,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs index d0e3d57..8e0c70a 100644 --- a/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; #[cfg(test)] @@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/add_series_handler.rs b/src/handlers/sonarr_handlers/library/add_series_handler.rs index e6f8162..80d8b81 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler.rs @@ -5,7 +5,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, ActiveSonarrBlock, }; use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult}; -use crate::models::{BlockSelectionState, Scrollable}; +use crate::models::{BlockSelectionState, Route, Scrollable}; use crate::network::sonarr_network::SonarrEvent; use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -625,7 +625,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/delete_series_handler.rs b/src/handlers/sonarr_handlers/library/delete_series_handler.rs index 97fde80..6b6dac5 100644 --- a/src/handlers/sonarr_handlers/library/delete_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/delete_series_handler.rs @@ -7,6 +7,7 @@ use crate::{ matches_key, models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}, }; +use crate::models::Route; #[cfg(test)] #[path = "delete_series_handler_tests.rs"] @@ -143,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler< self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/edit_series_handler.rs b/src/handlers/sonarr_handlers/library/edit_series_handler.rs index a57224e..7e872d8 100644 --- a/src/handlers/sonarr_handlers/library/edit_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/edit_series_handler.rs @@ -1,7 +1,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; -use crate::models::Scrollable; +use crate::models::{Route, Scrollable}; use crate::models::servarr_data::sonarr::modals::EditSeriesModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS}; use crate::models::sonarr_models::EditSeriesParams; @@ -471,7 +471,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/episode_details_handler.rs b/src/handlers/sonarr_handlers/library/episode_details_handler.rs index c007778..107cc59 100644 --- a/src/handlers/sonarr_handlers/library/episode_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/episode_details_handler.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::library::season_details_handler::releases_ use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS}; use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody}; use crate::network::sonarr_network::SonarrEvent; @@ -370,7 +371,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandle self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/mod.rs b/src/handlers/sonarr_handlers/library/mod.rs index dc60579..01934a4 100644 --- a/src/handlers/sonarr_handlers/library/mod.rs +++ b/src/handlers/sonarr_handlers/library/mod.rs @@ -25,6 +25,7 @@ use crate::handlers::sonarr_handlers::library::episode_details_handler::EpisodeD use crate::handlers::sonarr_handlers::library::season_details_handler::SeasonDetailsHandler; use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; mod add_series_handler; mod delete_series_handler; @@ -245,7 +246,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/season_details_handler.rs b/src/handlers/sonarr_handlers/library/season_details_handler.rs index 193c3ff..2d4b2e1 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler.rs @@ -12,6 +12,7 @@ use crate::models::sonarr_models::{ use crate::models::stateful_table::SortOption; use crate::network::sonarr_network::SonarrEvent; use serde_json::Number; +use crate::models::Route; #[cfg(test)] #[path = "season_details_handler_tests.rs"] @@ -458,7 +459,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs index ee60e3e..478ef3f 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -4,7 +4,7 @@ use crate::handlers::sonarr_handlers::history::history_sorting_options; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; -use crate::models::BlockSelectionState; +use crate::models::{BlockSelectionState, Route}; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS, }; @@ -342,7 +342,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/mod.rs b/src/handlers/sonarr_handlers/mod.rs index 033392e..e33eecb 100644 --- a/src/handlers/sonarr_handlers/mod.rs +++ b/src/handlers/sonarr_handlers/mod.rs @@ -9,7 +9,7 @@ use system::SystemHandler; use crate::{ app::App, event::Key, matches_key, models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock, }; - +use crate::models::Route; use super::KeyEventHandler; mod blocklist; @@ -115,7 +115,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/root_folders/mod.rs b/src/handlers/sonarr_handlers/root_folders/mod.rs index 04da09c..9d6a3ee 100644 --- a/src/handlers/sonarr_handlers/root_folders/mod.rs +++ b/src/handlers/sonarr_handlers/root_folders/mod.rs @@ -3,7 +3,7 @@ use crate::event::Key; use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; -use crate::models::HorizontallyScrollableText; +use crate::models::{HorizontallyScrollableText, Route}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; use crate::models::servarr_models::AddRootFolderBody; use crate::network::sonarr_network::SonarrEvent; @@ -229,7 +229,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/system/mod.rs b/src/handlers/sonarr_handlers/system/mod.rs index 9c70ac4..34c0fff 100644 --- a/src/handlers/sonarr_handlers/system/mod.rs +++ b/src/handlers/sonarr_handlers/system/mod.rs @@ -4,7 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::sonarr_handlers::system::system_details_handler::SystemDetailsHandler; use crate::handlers::{KeyEventHandler, handle_clear_errors}; use crate::matches_key; -use crate::models::Scrollable; +use crate::models::{Route, Scrollable}; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; mod system_details_handler; @@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/system/system_details_handler.rs b/src/handlers/sonarr_handlers/system/system_details_handler.rs index 57879e2..3b79e0d 100644 --- a/src/handlers/sonarr_handlers/system/system_details_handler.rs +++ b/src/handlers/sonarr_handlers/system/system_details_handler.rs @@ -2,7 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; -use crate::models::Scrollable; +use crate::models::{Route, Scrollable}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::sonarr_models::SonarrTaskName; use crate::models::stateful_list::StatefulList; @@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/table_handler_tests.rs b/src/handlers/table_handler_tests.rs index 4e5b9ab..fb0585d 100644 --- a/src/handlers/table_handler_tests.rs +++ b/src/handlers/table_handler_tests.rs @@ -14,6 +14,7 @@ mod tests { use crate::models::servarr_models::Language; use crate::models::stateful_table::SortOption; use rstest::rstest; + use crate::models::Route; struct TableHandlerUnit<'a, 'b> { key: Key, @@ -98,7 +99,7 @@ mod tests { self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index 945c689..56278b0 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -118,6 +118,7 @@ impl From<(&i64, &String)> for MetadataProfile { Copy, Debug, EnumIter, + clap::ValueEnum, Display, EnumDisplayStyle, )] @@ -205,6 +206,21 @@ pub struct DeleteArtistParams { pub add_import_list_exclusion: bool, } +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct EditArtistParams { + pub artist_id: i64, + pub monitored: Option, + pub monitor_new_items: Option, + pub quality_profile_id: Option, + pub metadata_profile_id: Option, + pub root_folder_path: Option, + pub tags: Option>, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, + pub clear_tags: bool, +} + impl From for Serdeable { fn from(value: LidarrSerdeable) -> Serdeable { Serdeable::Lidarr(value) @@ -223,6 +239,7 @@ serde_enum_from!( RootFolders(Vec), SecurityConfig(SecurityConfig), SystemStatus(SystemStatus), + Tag(Tag), Tags(Vec), Value(Value), } diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs index e2f6cb6..bc54f32 100644 --- a/src/models/servarr_data/lidarr/lidarr_data.rs +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -1,3 +1,5 @@ +use serde_json::Number; + use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES; use crate::models::{ BlockSelectionState, Route, TabRoute, TabState, @@ -8,9 +10,17 @@ use crate::models::{ use crate::network::lidarr_network::LidarrEvent; use bimap::BiMap; use chrono::{DateTime, Utc}; -use strum::EnumIter; +use strum::{EnumIter}; +use super::modals::EditArtistModal; #[cfg(test)] -use strum::{Display, EnumString}; +use { + strum::{Display, EnumString, IntoEnumIterator}, + crate::models::lidarr_models::NewItemMonitorType, + crate::models::stateful_table::SortOption, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map, + crate::network::servarr_test_utils::diskspace, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{download_record, metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map}, +}; #[cfg(test)] #[path = "lidarr_data_tests.rs"] @@ -22,6 +32,7 @@ pub struct LidarrData<'a> { pub delete_artist_files: bool, pub disk_space_vec: Vec, pub downloads: StatefulTable, + pub edit_artist_modal: Option, pub main_tabs: TabState, pub metadata_profile_map: BiMap, pub prompt_confirm: bool, @@ -39,6 +50,31 @@ impl LidarrData<'_> { self.delete_artist_files = false; self.add_import_list_exclusion = false; } + + pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String { + tag_ids + .iter() + .filter_map(|id| { + let id = id.as_i64()?; + self.tags_map.get_by_left(&id).cloned() + }) + .collect::>() + .join(", ") + } + + pub fn sorted_quality_profile_names(&self) -> Vec { + let mut quality_profile_names: Vec = + self.quality_profile_map.right_values().cloned().collect(); + quality_profile_names.sort(); + quality_profile_names + } + + pub fn sorted_metadata_profile_names(&self) -> Vec { + let mut metadata_profile_names: Vec = + self.metadata_profile_map.right_values().cloned().collect(); + metadata_profile_names.sort(); + metadata_profile_names + } } impl<'a> Default for LidarrData<'a> { @@ -49,6 +85,7 @@ impl<'a> Default for LidarrData<'a> { delete_artist_files: false, disk_space_vec: Vec::new(), downloads: StatefulTable::default(), + edit_artist_modal: None, metadata_profile_map: BiMap::new(), prompt_confirm: false, prompt_confirm_action: None, @@ -71,11 +108,25 @@ impl<'a> Default for LidarrData<'a> { #[cfg(test)] impl LidarrData<'_> { pub fn test_default_fully_populated() -> Self { - use crate::models::lidarr_models::{Artist, DownloadRecord}; - use crate::models::servarr_models::{DiskSpace, RootFolder}; - use crate::models::stateful_table::SortOption; + let mut edit_artist_modal = EditArtistModal { + monitored: Some(true), + path: "/nfs/music".into(), + tags: "alex".into(), + ..EditArtistModal::default() + }; + edit_artist_modal.monitor_list.set_items(NewItemMonitorType::iter().collect()); + edit_artist_modal.quality_profile_list.set_items(vec![quality_profile().name]); + edit_artist_modal.metadata_profile_list.set_items(vec![metadata_profile().name]); - let mut lidarr_data = LidarrData::default(); + let mut lidarr_data = LidarrData { + delete_artist_files: true, + disk_space_vec: vec![diskspace()], + quality_profile_map: quality_profile_map(), + metadata_profile_map: metadata_profile_map(), + edit_artist_modal: Some(edit_artist_modal), + tags_map: tags_map(), + ..LidarrData::default() + }; lidarr_data.artists.set_items(vec![Artist::default()]); lidarr_data.artists.sorting(vec![SortOption { name: "Name", @@ -83,19 +134,12 @@ impl LidarrData<'_> { }]); lidarr_data.artists.search = Some("artist search".into()); lidarr_data.artists.filter = Some("artist filter".into()); - lidarr_data.quality_profile_map = BiMap::from_iter([(1i64, "Lossless".to_owned())]); - lidarr_data.metadata_profile_map = BiMap::from_iter([(1i64, "Standard".to_owned())]); - lidarr_data.tags_map = BiMap::from_iter([(1i64, "usenet".to_owned())]); - lidarr_data.disk_space_vec = vec![DiskSpace { - free_space: 50000000000, - total_space: 100000000000, - }]; lidarr_data .downloads - .set_items(vec![DownloadRecord::default()]); + .set_items(vec![download_record()]); lidarr_data .root_folders - .set_items(vec![RootFolder::default()]); + .set_items(vec![root_folder()]); lidarr_data.version = "1.0.0".to_owned(); lidarr_data @@ -112,6 +156,14 @@ pub enum ActiveLidarrBlock { DeleteArtistConfirmPrompt, DeleteArtistToggleDeleteFile, DeleteArtistToggleAddListExclusion, + EditArtistPrompt, + EditArtistConfirmPrompt, + EditArtistPathInput, + EditArtistSelectMetadataProfile, + EditArtistSelectMonitorNewItems, + EditArtistSelectQualityProfile, + EditArtistTagsInput, + EditArtistToggleMonitored, FilterArtists, FilterArtistsError, SearchArtists, @@ -142,6 +194,27 @@ pub const DELETE_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ &[ActiveLidarrBlock::DeleteArtistConfirmPrompt], ]; +pub static EDIT_ARTIST_BLOCKS: [ActiveLidarrBlock; 8] = [ + ActiveLidarrBlock::EditArtistPrompt, + ActiveLidarrBlock::EditArtistConfirmPrompt, + ActiveLidarrBlock::EditArtistPathInput, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + ActiveLidarrBlock::EditArtistTagsInput, + ActiveLidarrBlock::EditArtistToggleMonitored, +]; + +pub const EDIT_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::EditArtistToggleMonitored], + &[ActiveLidarrBlock::EditArtistSelectMonitorNewItems], + &[ActiveLidarrBlock::EditArtistSelectQualityProfile], + &[ActiveLidarrBlock::EditArtistSelectMetadataProfile], + &[ActiveLidarrBlock::EditArtistPathInput], + &[ActiveLidarrBlock::EditArtistTagsInput], + &[ActiveLidarrBlock::EditArtistConfirmPrompt], +]; + impl From for Route { fn from(active_lidarr_block: ActiveLidarrBlock) -> Route { Route::Lidarr(active_lidarr_block, None) diff --git a/src/models/servarr_data/lidarr/lidarr_data_tests.rs b/src/models/servarr_data/lidarr/lidarr_data_tests.rs index 5e032c5..182bf48 100644 --- a/src/models/servarr_data/lidarr/lidarr_data_tests.rs +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -1,15 +1,15 @@ #[cfg(test)] mod tests { + use bimap::BiMap; use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES; - use crate::models::servarr_data::lidarr::lidarr_data::{ - DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, - }; + use crate::models::servarr_data::lidarr::lidarr_data::{DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS}; use crate::models::{ BlockSelectionState, Route, servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS, LidarrData}, }; use chrono::{DateTime, Utc}; use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::Number; #[test] fn test_from_active_lidarr_block_to_route() { @@ -41,6 +41,50 @@ mod tests { assert!(!lidarr_data.add_import_list_exclusion); } + #[test] + fn test_tag_ids_to_display() { + let mut tags_map = BiMap::new(); + tags_map.insert(3, "test 3".to_owned()); + tags_map.insert(2, "test 2".to_owned()); + tags_map.insert(1, "test 1".to_owned()); + let lidarr_data = LidarrData { + tags_map, + ..LidarrData::default() + }; + + assert_str_eq!(lidarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), "test 1, test 2"); + } + + #[test] + fn test_sorted_quality_profile_names() { + let mut quality_profile_map = BiMap::new(); + quality_profile_map.insert(3, "test 3".to_owned()); + quality_profile_map.insert(2, "test 2".to_owned()); + quality_profile_map.insert(1, "test 1".to_owned()); + let lidarr_data = LidarrData { + quality_profile_map, + ..LidarrData::default() + }; + let expected_quality_profile_vec = vec!["test 1".to_owned(), "test 2".to_owned(), "test 3".to_owned()]; + + assert_iter_eq!(lidarr_data.sorted_quality_profile_names(), expected_quality_profile_vec); + } + + #[test] + fn test_sorted_metadata_profile_names() { + let mut metadata_profile_map = BiMap::new(); + metadata_profile_map.insert(3, "test 3".to_owned()); + metadata_profile_map.insert(2, "test 2".to_owned()); + metadata_profile_map.insert(1, "test 1".to_owned()); + let lidarr_data = LidarrData { + metadata_profile_map, + ..LidarrData::default() + }; + let expected_metadata_profile_vec = vec!["test 1".to_owned(), "test 2".to_owned(), "test 3".to_owned()]; + + assert_iter_eq!(lidarr_data.sorted_metadata_profile_names(), expected_metadata_profile_vec); + } + #[test] fn test_lidarr_data_default() { let lidarr_data = LidarrData::default(); @@ -50,6 +94,7 @@ mod tests { assert!(!lidarr_data.delete_artist_files); assert_is_empty!(lidarr_data.disk_space_vec); assert_is_empty!(lidarr_data.downloads); + assert_none!(lidarr_data.edit_artist_modal); assert_is_empty!(lidarr_data.metadata_profile_map); assert!(!lidarr_data.prompt_confirm); assert_none!(lidarr_data.prompt_confirm_action); @@ -113,4 +158,31 @@ mod tests { ); assert_none!(delete_artist_block_iter.next()); } + + #[test] + fn test_edit_artist_blocks() { + assert_eq!(EDIT_ARTIST_BLOCKS.len(), 8); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistPrompt)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistConfirmPrompt)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistPathInput)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectMetadataProfile)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectMonitorNewItems)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectQualityProfile)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistTagsInput)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistToggleMonitored)); + } + + #[test] + fn test_edit_artist_selection_blocks_ordering() { + let mut edit_artist_block_iter = EDIT_ARTIST_SELECTION_BLOCKS.iter(); + + assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistToggleMonitored]); + assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistSelectMonitorNewItems]); + assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistSelectQualityProfile]); + assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistSelectMetadataProfile]); + assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistPathInput]); + assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistTagsInput]); + assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistConfirmPrompt]); + assert_none!(edit_artist_block_iter.next()); + } } diff --git a/src/models/servarr_data/lidarr/mod.rs b/src/models/servarr_data/lidarr/mod.rs index 81f6a27..2db4fdf 100644 --- a/src/models/servarr_data/lidarr/mod.rs +++ b/src/models/servarr_data/lidarr/mod.rs @@ -1 +1,2 @@ pub mod lidarr_data; +pub mod modals; diff --git a/src/models/servarr_data/lidarr/modals.rs b/src/models/servarr_data/lidarr/modals.rs new file mode 100644 index 0000000..b769d65 --- /dev/null +++ b/src/models/servarr_data/lidarr/modals.rs @@ -0,0 +1,78 @@ +use strum::IntoEnumIterator; + +use super::lidarr_data::LidarrData; +use crate::models::{ + HorizontallyScrollableText, lidarr_models::NewItemMonitorType, stateful_list::StatefulList, +}; + +#[cfg(test)] +#[path = "modals_tests.rs"] +mod modals_tests; + +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct EditArtistModal { + pub monitor_list: StatefulList, + pub quality_profile_list: StatefulList, + pub metadata_profile_list: StatefulList, + pub monitored: Option, + pub path: HorizontallyScrollableText, + pub tags: HorizontallyScrollableText, +} + +impl From<&LidarrData<'_>> for EditArtistModal { + fn from(lidarr_data: &LidarrData<'_>) -> EditArtistModal { + let mut edit_artist_modal = EditArtistModal::default(); + let artist = lidarr_data.artists.current_selection(); + + edit_artist_modal + .monitor_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + edit_artist_modal.path = artist.path.clone().into(); + edit_artist_modal.tags = lidarr_data.tag_ids_to_display(&artist.tags).into(); + edit_artist_modal.monitored = Some(artist.monitored); + + let monitor_index = edit_artist_modal + .monitor_list + .items + .iter() + .position(|m| *m == artist.monitor_new_items); + edit_artist_modal.monitor_list.state.select(monitor_index); + + edit_artist_modal + .quality_profile_list + .set_items(lidarr_data.sorted_quality_profile_names()); + let quality_profile_name = lidarr_data + .quality_profile_map + .get_by_left(&artist.quality_profile_id) + .unwrap(); + let quality_profile_index = edit_artist_modal + .quality_profile_list + .items + .iter() + .position(|profile| profile == quality_profile_name); + edit_artist_modal + .quality_profile_list + .state + .select(quality_profile_index); + + edit_artist_modal + .metadata_profile_list + .set_items(lidarr_data.sorted_metadata_profile_names()); + let metadata_profile_name = lidarr_data + .metadata_profile_map + .get_by_left(&artist.metadata_profile_id) + .unwrap(); + let metadata_profile_index = edit_artist_modal + .metadata_profile_list + .items + .iter() + .position(|profile| profile == metadata_profile_name); + edit_artist_modal + .metadata_profile_list + .state + .select(metadata_profile_index); + + edit_artist_modal + } +} diff --git a/src/models/servarr_data/lidarr/modals_tests.rs b/src/models/servarr_data/lidarr/modals_tests.rs new file mode 100644 index 0000000..0889091 --- /dev/null +++ b/src/models/servarr_data/lidarr/modals_tests.rs @@ -0,0 +1,48 @@ +#[cfg(test)] +mod tests { + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::models::lidarr_models::{Artist, NewItemMonitorType}; + use crate::models::servarr_data::lidarr::lidarr_data::LidarrData; + use crate::models::servarr_data::lidarr::modals::EditArtistModal; + + #[test] + fn test_edit_artist_modal_from_lidarr_data() { + let mut lidarr_data = LidarrData { + quality_profile_map: BiMap::from_iter([(1i64, "HD - 1080p".to_owned()), (2i64, "Any".to_owned())]), + metadata_profile_map: BiMap::from_iter([(1i64, "Standard".to_owned()), (2i64, "None".to_owned())]), + tags_map: BiMap::from_iter([(1i64, "usenet".to_owned())]), + ..LidarrData::default() + }; + let artist = Artist { + id: 1, + monitored: true, + monitor_new_items: NewItemMonitorType::All, + quality_profile_id: 1, + metadata_profile_id: 1, + path: "/nfs/music/test_artist".to_owned(), + tags: vec![serde_json::Number::from(1)], + ..Artist::default() + }; + lidarr_data.artists.set_items(vec![artist]); + + let edit_artist_modal = EditArtistModal::from(&lidarr_data); + + assert_eq!(edit_artist_modal.monitored, Some(true)); + assert_eq!( + *edit_artist_modal.monitor_list.current_selection(), + NewItemMonitorType::All + ); + assert_str_eq!( + edit_artist_modal.quality_profile_list.current_selection(), + "HD - 1080p" + ); + assert_str_eq!( + edit_artist_modal.metadata_profile_list.current_selection(), + "Standard" + ); + assert_str_eq!(edit_artist_modal.path.text, "/nfs/music/test_artist"); + assert_str_eq!(edit_artist_modal.tags.text, "usenet"); + } +} diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index b56d232..15c8b6d 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -1,9 +1,10 @@ #[cfg(test)] mod tests { mod radarr_data_tests { + use bimap::BiMap; use chrono::{DateTime, Utc}; use pretty_assertions::{assert_eq, assert_str_eq}; - + use serde_json::Number; use crate::app::context_clues::{ BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, @@ -61,6 +62,35 @@ mod tests { assert_movie_info_tabs_reset!(radarr_data); } + #[test] + fn test_tag_ids_to_display() { + let mut tags_map = BiMap::new(); + tags_map.insert(3, "test 3".to_owned()); + tags_map.insert(2, "test 2".to_owned()); + tags_map.insert(1, "test 1".to_owned()); + let radarr_data = RadarrData { + tags_map, + ..RadarrData::default() + }; + + assert_str_eq!(radarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), "test 1, test 2"); + } + + #[test] + fn test_sorted_quality_profile_names() { + let mut quality_profile_map = BiMap::new(); + quality_profile_map.insert(3, "test 3".to_owned()); + quality_profile_map.insert(2, "test 2".to_owned()); + quality_profile_map.insert(1, "test 1".to_owned()); + let radarr_data = RadarrData { + quality_profile_map, + ..RadarrData::default() + }; + let expected_quality_profile_vec = vec!["test 1".to_owned(), "test 2".to_owned(), "test 3".to_owned()]; + + assert_iter_eq!(radarr_data.sorted_quality_profile_names(), expected_quality_profile_vec); + } + #[test] fn test_radarr_data_defaults() { let radarr_data = RadarrData::default(); diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index 2a70d40..641a5f6 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -1,9 +1,10 @@ #[cfg(test)] mod tests { mod sonarr_data_tests { + use bimap::BiMap; use chrono::{DateTime, Utc}; use pretty_assertions::{assert_eq, assert_str_eq}; - + use serde_json::Number; use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES; use crate::models::sonarr_models::{Season, SonarrHistoryItem}; use crate::models::stateful_table::StatefulTable; @@ -77,6 +78,50 @@ mod tests { assert_eq!(sonarr_data.series_info_tabs.index, 0); } + #[test] + fn test_tag_ids_to_display() { + let mut tags_map = BiMap::new(); + tags_map.insert(3, "test 3".to_owned()); + tags_map.insert(2, "test 2".to_owned()); + tags_map.insert(1, "test 1".to_owned()); + let sonarr_data = SonarrData { + tags_map, + ..SonarrData::default() + }; + + assert_str_eq!(sonarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), "test 1, test 2"); + } + + #[test] + fn test_sorted_quality_profile_names() { + let mut quality_profile_map = BiMap::new(); + quality_profile_map.insert(3, "test 3".to_owned()); + quality_profile_map.insert(2, "test 2".to_owned()); + quality_profile_map.insert(1, "test 1".to_owned()); + let sonarr_data = SonarrData { + quality_profile_map, + ..SonarrData::default() + }; + let expected_quality_profile_vec = vec!["test 1".to_owned(), "test 2".to_owned(), "test 3".to_owned()]; + + assert_iter_eq!(sonarr_data.sorted_quality_profile_names(), expected_quality_profile_vec); + } + + #[test] + fn test_sorted_language_profile_names() { + let mut language_profiles_map = BiMap::new(); + language_profiles_map.insert(3, "test 3".to_owned()); + language_profiles_map.insert(2, "test 2".to_owned()); + language_profiles_map.insert(1, "test 1".to_owned()); + let sonarr_data = SonarrData { + language_profiles_map, + ..SonarrData::default() + }; + let expected_language_profiles_vec = vec!["test 1".to_owned(), "test 2".to_owned(), "test 3".to_owned()]; + + assert_iter_eq!(sonarr_data.sorted_language_profile_names(), expected_language_profiles_vec); + } + #[test] fn test_sonarr_data_defaults() { let sonarr_data = SonarrData::default(); diff --git a/src/network/lidarr_network/library/mod.rs b/src/network/lidarr_network/library/mod.rs index 9765cd1..a9264e0 100644 --- a/src/network/lidarr_network/library/mod.rs +++ b/src/network/lidarr_network/library/mod.rs @@ -3,7 +3,7 @@ use log::{debug, info, warn}; use serde_json::{Value, json}; use crate::models::Route; -use crate::models::lidarr_models::{Artist, DeleteArtistParams}; +use crate::models::lidarr_models::{Artist, DeleteArtistParams, EditArtistParams}; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_models::CommandBody; use crate::network::lidarr_network::LidarrEvent; @@ -168,4 +168,119 @@ impl Network<'_, '_> { .handle_request::(request_props, |_, _| ()) .await } + + pub(in crate::network::lidarr_network) async fn edit_artist( + &mut self, + mut edit_artist_params: EditArtistParams, + ) -> Result<()> { + info!("Editing Lidarr artist"); + if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() { + let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await; + edit_artist_params.tags = Some(tag_ids_vec); + } + let artist_id = edit_artist_params.artist_id; + let detail_event = LidarrEvent::GetArtistDetails(artist_id); + let event = LidarrEvent::EditArtist(EditArtistParams::default()); + info!("Fetching artist details for artist with ID: {artist_id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{artist_id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_artist_body, _| { + response = detailed_artist_body.to_string() + }) + .await?; + + info!("Constructing edit artist body"); + + let mut detailed_artist_body: Value = serde_json::from_str(&response)?; + let ( + monitored, + monitor_new_items, + quality_profile_id, + metadata_profile_id, + root_folder_path, + tags, + ) = { + let monitored = edit_artist_params.monitored.unwrap_or( + detailed_artist_body["monitored"] + .as_bool() + .expect("Unable to deserialize 'monitored'"), + ); + let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| { + serde_json::from_value(detailed_artist_body["monitorNewItems"].clone()) + .expect("Unable to deserialize 'monitorNewItems'") + }); + let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| { + detailed_artist_body["qualityProfileId"] + .as_i64() + .expect("Unable to deserialize 'qualityProfileId'") + }); + let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| { + detailed_artist_body["metadataProfileId"] + .as_i64() + .expect("Unable to deserialize 'metadataProfileId'") + }); + let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| { + detailed_artist_body["path"] + .as_str() + .expect("Unable to deserialize 'path'") + .to_owned() + }); + let tags = if edit_artist_params.clear_tags { + vec![] + } else { + edit_artist_params.tags.unwrap_or( + detailed_artist_body["tags"] + .as_array() + .expect("Unable to deserialize 'tags'") + .iter() + .map(|item| item.as_i64().expect("Unable to deserialize tag ID")) + .collect(), + ) + }; + + ( + monitored, + monitor_new_items, + quality_profile_id, + metadata_profile_id, + root_folder_path, + tags, + ) + }; + + *detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items); + *detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); + *detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id); + *detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path); + *detailed_artist_body.get_mut("tags").unwrap() = json!(tags); + + debug!("Edit artist body: {detailed_artist_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_artist_body), + Some(format!("/{artist_id}")), + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } } diff --git a/src/network/lidarr_network/lidarr_network_test_utils.rs b/src/network/lidarr_network/lidarr_network_test_utils.rs new file mode 100644 index 0000000..8677e6c --- /dev/null +++ b/src/network/lidarr_network/lidarr_network_test_utils.rs @@ -0,0 +1,132 @@ +#[cfg(test)] +#[allow(dead_code)] // TODO: maybe remove? +pub mod test_utils { + use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus, DownloadRecord, DownloadStatus, DownloadsResponse, Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus}; + use crate::models::servarr_models::{QualityProfile, RootFolder, Tag}; + use crate::models::HorizontallyScrollableText; + use bimap::BiMap; + use chrono::DateTime; + use serde_json::Number; + + pub fn member() -> Member { + Member { + name: Some("alex".to_owned()), + instrument: Some("piano".to_owned()) + } + } + + pub fn ratings() -> Ratings { + Ratings { + votes: 15, + value: 8.4 + } + } + + pub fn artist_statistics() -> ArtistStatistics { + ArtistStatistics { + album_count: 1, + track_file_count: 15, + track_count: 15, + total_track_count: 15, + size_on_disk: 12345, + percent_of_tracks: 99.9 + } + } + + pub fn artist() -> Artist { + Artist { + id: 1, + artist_name: "Alex".into(), + foreign_artist_id: "test-foreign-id".to_owned(), + status: ArtistStatus::Continuing, + overview: Some("some interesting description of the artist".to_owned()), + artist_type: Some("Person".to_owned()), + disambiguation: Some("American pianist".to_owned()), + members: Some(vec![member()]), + path: "/nfs/music/test-artist".to_owned(), + quality_profile_id: quality_profile().id, + metadata_profile_id: metadata_profile().id, + monitored: true, + monitor_new_items: NewItemMonitorType::All, + genres: vec!["soundtrack".to_owned()], + tags: vec![Number::from(tag().id)], + added: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()), + ratings: Some(ratings()), + statistics: Some(artist_statistics()) + } + } + + pub fn quality_profile() -> QualityProfile { + QualityProfile { + id: 1, + name: "Lossless".to_owned() + } + } + + pub fn quality_profile_map() -> BiMap { + let quality_profile = quality_profile(); + BiMap::from_iter(vec![(quality_profile.id, quality_profile.name)]) + } + + pub fn metadata_profile() -> MetadataProfile { + MetadataProfile { + id: 1, + name: "Standard".to_owned() + } + } + + pub fn metadata_profile_map() -> BiMap { + let metadata_profile = metadata_profile(); + BiMap::from_iter(vec![(metadata_profile.id, metadata_profile.name)]) + } + + pub fn tag() -> Tag { + Tag { + id: 1, + label: "alex".to_owned() + } + } + + pub fn tags_map() -> BiMap { + let tag = tag(); + BiMap::from_iter(vec![(tag.id, tag.label)]) + } + + pub fn download_record() -> DownloadRecord { + DownloadRecord { + title: "Test download title".to_owned(), + status: DownloadStatus::Downloading, + id: 1, + album_id: Some(Number::from(1i64)), + artist_id: Some(Number::from(1i64)), + size: 3543348019f64, + sizeleft: 1771674009f64, + output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")), + indexer: "kickass torrents".to_owned(), + download_client: Some("transmission".to_owned()) + } + } + + pub fn downloads_response() -> DownloadsResponse { + DownloadsResponse { + records: vec![download_record()] + } + } + + pub fn system_status() -> SystemStatus { + SystemStatus { + version: "1.0".to_owned(), + start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()), + } + } + + pub fn root_folder() -> RootFolder { + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + } + } +} \ No newline at end of file diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index bdb5810..068a090 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -2,7 +2,9 @@ use anyhow::Result; use log::info; use super::{NetworkEvent, NetworkResource}; -use crate::models::lidarr_models::{DeleteArtistParams, LidarrSerdeable, MetadataProfile}; +use crate::models::lidarr_models::{ + DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile, +}; use crate::models::servarr_models::{QualityProfile, Tag}; use crate::network::{Network, RequestMethod}; @@ -15,9 +17,15 @@ mod system; #[path = "lidarr_network_tests.rs"] mod lidarr_network_tests; +#[cfg(test)] +#[path = "lidarr_network_test_utils.rs"] +pub mod lidarr_network_test_utils; + #[derive(Debug, Eq, PartialEq, Clone)] pub enum LidarrEvent { + AddTag(String), DeleteArtist(DeleteArtistParams), + EditArtist(EditArtistParams), GetArtistDetails(i64), GetDiskSpace, GetDownloads(u64), @@ -37,7 +45,9 @@ pub enum LidarrEvent { impl NetworkResource for LidarrEvent { fn resource(&self) -> &'static str { match &self { + LidarrEvent::AddTag(_) | LidarrEvent::GetTags => "/tag", LidarrEvent::DeleteArtist(_) + | LidarrEvent::EditArtist(_) | LidarrEvent::GetArtistDetails(_) | LidarrEvent::ListArtists | LidarrEvent::ToggleArtistMonitoring(_) => "/artist", @@ -49,7 +59,6 @@ impl NetworkResource for LidarrEvent { LidarrEvent::GetQualityProfiles => "/qualityprofile", LidarrEvent::GetRootFolders => "/rootfolder", LidarrEvent::GetStatus => "/system/status", - LidarrEvent::GetTags => "/tag", LidarrEvent::HealthCheck => "/health", } } @@ -67,6 +76,7 @@ impl Network<'_, '_> { lidarr_event: LidarrEvent, ) -> Result { match lidarr_event { + LidarrEvent::AddTag(tag) => self.add_lidarr_tag(tag).await.map(LidarrSerdeable::from), LidarrEvent::DeleteArtist(params) => { self.delete_artist(params).await.map(LidarrSerdeable::from) } @@ -111,6 +121,7 @@ impl Network<'_, '_> { .await .map(LidarrSerdeable::from), LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from), + LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from), } } @@ -180,4 +191,61 @@ impl Network<'_, '_> { }) .await } + + async fn add_lidarr_tag(&mut self, tag: String) -> Result { + info!("Adding a new Lidarr tag"); + let event = LidarrEvent::AddTag(String::new()); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + Some(serde_json::json!({ "label": tag })), + None, + None, + ) + .await; + + self + .handle_request::(request_props, |tag, mut app| { + app.data.lidarr_data.tags_map.insert(tag.id, tag.label); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn extract_and_add_lidarr_tag_ids_vec( + &mut self, + edit_tags: &str, + ) -> Vec { + let missing_tags_vec = { + let tags_map = &self.app.lock().await.data.lidarr_data.tags_map; + edit_tags + .split(',') + .filter(|&tag| { + !tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none() + }) + .collect::>() + }; + + for tag in missing_tags_vec { + self + .add_lidarr_tag(tag.trim().to_owned()) + .await + .expect("Unable to add tag"); + } + + let app = self.app.lock().await; + edit_tags + .split(',') + .filter(|tag| !tag.is_empty()) + .map(|tag| { + *app + .data + .lidarr_data + .tags_map + .get_by_right(tag.to_lowercase().trim()) + .unwrap() + }) + .collect() + } } diff --git a/src/network/sonarr_network/sonarr_network_test_utils.rs b/src/network/sonarr_network/sonarr_network_test_utils.rs index e3fc28c..f2d732e 100644 --- a/src/network/sonarr_network/sonarr_network_test_utils.rs +++ b/src/network/sonarr_network/sonarr_network_test_utils.rs @@ -284,6 +284,7 @@ pub mod test_utils { subtitles: Some("English".to_owned()), } } + pub fn quality() -> Quality { Quality { name: "Bluray-1080p".to_owned(), diff --git a/src/ui/lidarr_ui/library/edit_artist_ui.rs b/src/ui/lidarr_ui/library/edit_artist_ui.rs new file mode 100644 index 0000000..7137904 --- /dev/null +++ b/src/ui/lidarr_ui/library/edit_artist_ui.rs @@ -0,0 +1,222 @@ +use std::sync::atomic::Ordering; + +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::prelude::Layout; +use ratatui::widgets::ListItem; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS}; +use crate::models::servarr_data::lidarr::modals::EditArtistModal; +use crate::render_selectable_input_box; + +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{title_block_centered}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{DrawUi, draw_popup}; + +#[cfg(test)] +#[path = "edit_artist_ui_tests.rs"] +mod edit_artist_ui_tests; + +pub(super) struct EditArtistUi; + +impl DrawUi for EditArtistUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() { + let draw_edit_artist_prompt = |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| { + draw_edit_artist_confirmation_prompt(f, app, prompt_area); + + match active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => { + draw_edit_artist_select_monitor_new_items_popup(f, app); + } + ActiveLidarrBlock::EditArtistSelectQualityProfile => { + draw_edit_artist_select_quality_profile_popup(f, app); + } + ActiveLidarrBlock::EditArtistSelectMetadataProfile => { + draw_edit_artist_select_metadata_profile_popup(f, app); + } + _ => (), + } + }; + + draw_popup(f, app, draw_edit_artist_prompt, Size::Long); + } + } +} + +fn draw_edit_artist_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let artist_name = app + .data + .lidarr_data + .artists + .current_selection() + .artist_name + .text + .clone(); + let title = format!("Edit - {artist_name}"); + f.render_widget(title_block_centered(&title), area); + + 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::EditArtistConfirmPrompt; + let EditArtistModal { + monitor_list, + quality_profile_list, + metadata_profile_list, + monitored, + path, + tags, + } = app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .expect("edit_artist_modal must exist in this context"); + let selected_monitor_new_items = monitor_list.current_selection(); + let selected_quality_profile = quality_profile_list.current_selection(); + let selected_metadata_profile = metadata_profile_list.current_selection(); + + let [ + _, + monitored_area, + monitor_new_items_area, + quality_profile_area, + metadata_profile_area, + path_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 [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(buttons_area); + + let monitored_checkbox = Checkbox::new("Monitored") + .checked(monitored.unwrap_or_default()) + .highlighted(selected_block == ActiveLidarrBlock::EditArtistToggleMonitored); + let monitor_new_items_drop_down_button = Button::new() + .title(selected_monitor_new_items.to_display_str()) + .label("Monitor New Albums") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::EditArtistSelectMonitorNewItems); + let quality_profile_drop_down_button = Button::new() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::EditArtistSelectQualityProfile); + let metadata_profile_drop_down_button = Button::new() + .title(selected_metadata_profile) + .label("Metadata Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::EditArtistSelectMetadataProfile); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let path_input_box = InputBox::new(&path.text) + .offset(path.offset.load(Ordering::SeqCst)) + .label("Path") + .highlighted(selected_block == ActiveLidarrBlock::EditArtistPathInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditArtistPathInput); + let tags_input_box = InputBox::new(&tags.text) + .offset(tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::EditArtistTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditArtistTagsInput); + + match active_lidarr_block { + ActiveLidarrBlock::EditArtistPathInput => path_input_box.show_cursor(f, path_area), + ActiveLidarrBlock::EditArtistTagsInput => tags_input_box.show_cursor(f, tags_area), + _ => (), + } + + render_selectable_input_box!(path_input_box, f, path_area); + render_selectable_input_box!(tags_input_box, f, tags_area); + } + + let save_button = Button::new() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(monitored_checkbox, monitored_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); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); +} + +fn draw_edit_artist_select_monitor_new_items_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_list = SelectableList::new( + &mut app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .expect("edit_artist_modal must exist in this context") + .monitor_list, + |monitor_type| ListItem::new(monitor_type.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_edit_artist_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let quality_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .expect("edit_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_edit_artist_select_metadata_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let metadata_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .expect("edit_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()); +} diff --git a/src/ui/lidarr_ui/library/edit_artist_ui_tests.rs b/src/ui/lidarr_ui/library/edit_artist_ui_tests.rs new file mode 100644 index 0000000..2980d1d --- /dev/null +++ b/src/ui/lidarr_ui/library/edit_artist_ui_tests.rs @@ -0,0 +1,22 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use strum::IntoEnumIterator; + + use crate::models::Route; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::edit_artist_ui::EditArtistUi; + + #[test] + fn test_edit_artist_ui_accepts() { + let mut edit_artist_ui_blocks = Vec::new(); + for block in ActiveLidarrBlock::iter() { + if EditArtistUi::accepts(Route::Lidarr(block, None)) { + edit_artist_ui_blocks.push(block); + } + } + + assert_eq!(edit_artist_ui_blocks, EDIT_ARTIST_BLOCKS.to_vec()); + } +} diff --git a/src/ui/lidarr_ui/library/library_ui_tests.rs b/src/ui/lidarr_ui/library/library_ui_tests.rs index 1be94f4..645be1c 100644 --- a/src/ui/lidarr_ui/library/library_ui_tests.rs +++ b/src/ui/lidarr_ui/library/library_ui_tests.rs @@ -4,7 +4,7 @@ mod tests { use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus}; use crate::models::servarr_data::lidarr::lidarr_data::{ - ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, LIBRARY_BLOCKS, + ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS, }; use crate::ui::DrawUi; use crate::ui::lidarr_ui::library::{LibraryUi, decorate_artist_row_with_style}; @@ -17,6 +17,8 @@ mod tests { let mut library_ui_blocks = Vec::new(); library_ui_blocks.extend(LIBRARY_BLOCKS); library_ui_blocks.extend(DELETE_ARTIST_BLOCKS); + library_ui_blocks.extend(EDIT_ARTIST_BLOCKS); + for active_lidarr_block in ActiveLidarrBlock::iter() { if library_ui_blocks.contains(&active_lidarr_block) { assert!(LibraryUi::accepts(active_lidarr_block.into())); diff --git a/src/ui/lidarr_ui/library/mod.rs b/src/ui/lidarr_ui/library/mod.rs index 064181b..57c2c6c 100644 --- a/src/ui/lidarr_ui/library/mod.rs +++ b/src/ui/lidarr_ui/library/mod.rs @@ -1,4 +1,5 @@ use delete_artist_ui::DeleteArtistUi; +use edit_artist_ui::EditArtistUi; use ratatui::{ Frame, layout::{Constraint, Rect}, @@ -26,6 +27,7 @@ use crate::{ }; mod delete_artist_ui; +mod edit_artist_ui; #[cfg(test)] #[path = "library_ui_tests.rs"] @@ -36,7 +38,9 @@ pub(super) struct LibraryUi; impl DrawUi for LibraryUi { fn accepts(route: Route) -> bool { if let Route::Lidarr(active_lidarr_block, _) = route { - return DeleteArtistUi::accepts(route) || LIBRARY_BLOCKS.contains(&active_lidarr_block); + return DeleteArtistUi::accepts(route) + || EditArtistUi::accepts(route) + || LIBRARY_BLOCKS.contains(&active_lidarr_block); } false @@ -48,6 +52,7 @@ impl DrawUi for LibraryUi { match route { _ if DeleteArtistUi::accepts(route) => DeleteArtistUi::draw(f, app, area), + _ if EditArtistUi::accepts(route) => EditArtistUi::draw(f, app, area), Route::Lidarr(ActiveLidarrBlock::UpdateAllArtistsPrompt, _) => { let confirmation_prompt = ConfirmationPrompt::new() .title("Update All Artists")