From 6771a0ab3812b98de2795e0d6080a678d63e592d Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 5 Jan 2026 15:44:51 -0700 Subject: [PATCH] feat: Full support for deleting an artist via CLI and TUI --- src/app/lidarr/lidarr_context_clues.rs | 13 + src/app/lidarr/lidarr_context_clues_tests.rs | 8 +- src/app/lidarr/lidarr_tests.rs | 4 + src/app/lidarr/mod.rs | 2 +- src/app/mod.rs | 2 +- src/app/sonarr/sonarr_context_clues_tests.rs | 1 - src/cli/cli_tests.rs | 17 +- src/cli/lidarr/delete_command_handler.rs | 80 ++++ .../lidarr/delete_command_handler_tests.rs | 145 +++++++ src/cli/lidarr/lidarr_command_tests.rs | 87 ++++ src/cli/lidarr/mod.rs | 17 +- .../library/delete_artist_handler.rs | 149 +++++++ .../library/delete_artist_handler_tests.rs | 410 ++++++++++++++++++ src/handlers/lidarr_handlers/library/mod.rs | 19 +- src/handlers/lidarr_handlers/mod.rs | 6 +- src/models/lidarr_models.rs | 14 +- src/models/servarr_data/lidarr/lidarr_data.rs | 44 +- .../servarr_data/lidarr/lidarr_data_tests.rs | 19 +- src/models/servarr_data/mod.rs | 2 +- .../lidarr_downloads_network_tests.rs | 43 ++ src/network/lidarr_network/downloads/mod.rs | 40 ++ .../library/lidarr_library_network_tests.rs | 27 +- src/network/lidarr_network/library/mod.rs | 34 +- .../lidarr_network/lidarr_network_tests.rs | 113 ++++- src/network/lidarr_network/mod.rs | 68 ++- .../lidarr_root_folders_network_tests.rs | 39 ++ .../lidarr_network/root_folders/mod.rs | 29 ++ .../system/lidarr_system_network_tests.rs | 176 +------- src/network/lidarr_network/system/mod.rs | 107 +---- src/ui/lidarr_ui/library/delete_artist_ui.rs | 57 +++ .../library/delete_artist_ui_tests.rs | 44 ++ src/ui/lidarr_ui/library/library_ui_tests.rs | 245 ++++++++++- src/ui/lidarr_ui/library/mod.rs | 10 +- ...elete_artist_ui_renders_delete_artist.snap | 38 ++ ...ui_renders_delete_artist_over_library.snap | 38 ++ ...pshot_tests__library_ui_renders_empty.snap | 5 + ...hot_tests__library_ui_renders_loading.snap | 8 + ...napshot_tests__lidarr_library_Artists.snap | 7 + ...sts__lidarr_library_ArtistsSortPrompt.snap | 42 ++ ...t_tests__lidarr_library_FilterArtists.snap | 28 ++ ...ts__lidarr_library_FilterArtistsError.snap | 31 ++ ...t_tests__lidarr_library_SearchArtists.snap | 28 ++ ...ts__lidarr_library_SearchArtistsError.snap | 31 ++ 43 files changed, 1995 insertions(+), 332 deletions(-) create mode 100644 src/cli/lidarr/delete_command_handler.rs create mode 100644 src/cli/lidarr/delete_command_handler_tests.rs create mode 100644 src/handlers/lidarr_handlers/library/delete_artist_handler.rs create mode 100644 src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs create mode 100644 src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs create mode 100644 src/network/lidarr_network/downloads/mod.rs create mode 100644 src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs create mode 100644 src/network/lidarr_network/root_folders/mod.rs create mode 100644 src/ui/lidarr_ui/library/delete_artist_ui.rs create mode 100644 src/ui/lidarr_ui/library/delete_artist_ui_tests.rs create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_artist_ui__delete_artist_ui_tests__tests__snapshot_tests__delete_artist_ui_renders_delete_artist.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_delete_artist_over_library.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_Artists.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_ArtistsSortPrompt.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtists.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtistsError.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtists.snap create mode 100644 src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtistsError.snap diff --git a/src/app/lidarr/lidarr_context_clues.rs b/src/app/lidarr/lidarr_context_clues.rs index a2cdabe..772cb27 100644 --- a/src/app/lidarr/lidarr_context_clues.rs +++ b/src/app/lidarr/lidarr_context_clues.rs @@ -1,11 +1,24 @@ use crate::app::App; use crate::app::context_clues::{ContextClue, ContextClueProvider}; +use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::models::Route; #[cfg(test)] #[path = "lidarr_context_clues_tests.rs"] mod lidarr_context_clues_tests; +pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 6] = [ + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.esc, "cancel filter"), +]; + pub(in crate::app) struct LidarrContextClueProvider; impl ContextClueProvider for LidarrContextClueProvider { diff --git a/src/app/lidarr/lidarr_context_clues_tests.rs b/src/app/lidarr/lidarr_context_clues_tests.rs index 6f7f177..75e4193 100644 --- a/src/app/lidarr/lidarr_context_clues_tests.rs +++ b/src/app/lidarr/lidarr_context_clues_tests.rs @@ -2,10 +2,10 @@ mod tests { use crate::app::context_clues::ContextClueProvider; use crate::app::key_binding::DEFAULT_KEYBINDINGS; - use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider; + use crate::app::lidarr::lidarr_context_clues::{LidarrContextClueProvider, ARTISTS_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::lidarr::lidarr_data::{ - ActiveLidarrBlock, ARTISTS_CONTEXT_CLUES, + ActiveLidarrBlock, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; @@ -17,6 +17,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.delete, DEFAULT_KEYBINDINGS.delete.desc) + ); assert_some_eq_x!( artists_context_clues_iter.next(), &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) diff --git a/src/app/lidarr/lidarr_tests.rs b/src/app/lidarr/lidarr_tests.rs index 75b282e..60fe4c0 100644 --- a/src/app/lidarr/lidarr_tests.rs +++ b/src/app/lidarr/lidarr_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use crate::app::App; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::network::NetworkEvent; @@ -14,6 +15,7 @@ mod tests { app.dispatch_by_lidarr_block(&ActiveLidarrBlock::Artists).await; + assert!(app.is_loading); assert_eq!( rx.recv().await.unwrap(), LidarrEvent::GetQualityProfiles.into() @@ -24,5 +26,7 @@ mod tests { ); assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); } } diff --git a/src/app/lidarr/mod.rs b/src/app/lidarr/mod.rs index 4caaaea..716376f 100644 --- a/src/app/lidarr/mod.rs +++ b/src/app/lidarr/mod.rs @@ -5,7 +5,7 @@ use crate::{ use super::App; -pub(in crate::app) mod lidarr_context_clues; +pub mod lidarr_context_clues; #[cfg(test)] #[path = "lidarr_tests.rs"] diff --git a/src/app/mod.rs b/src/app/mod.rs index c40e0fb..7fcd297 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -26,9 +26,9 @@ mod app_tests; pub mod context_clues; pub mod key_binding; mod key_binding_tests; -pub mod lidarr; pub mod radarr; pub mod sonarr; +pub mod lidarr; pub struct App<'a> { navigation_stack: Vec, diff --git a/src/app/sonarr/sonarr_context_clues_tests.rs b/src/app/sonarr/sonarr_context_clues_tests.rs index 30b953d..08aa67f 100644 --- a/src/app/sonarr/sonarr_context_clues_tests.rs +++ b/src/app/sonarr/sonarr_context_clues_tests.rs @@ -455,7 +455,6 @@ mod tests { let mut app = App::test_default(); app.push_navigation_stack(ActiveRadarrBlock::default().into()); - // This should panic because the route is not a Sonarr route SonarrContextClueProvider::get_context_clues(&mut app); } diff --git a/src/cli/cli_tests.rs b/src/cli/cli_tests.rs index 7232497..2beb319 100644 --- a/src/cli/cli_tests.rs +++ b/src/cli/cli_tests.rs @@ -2,18 +2,16 @@ mod tests { use std::sync::Arc; - use clap::{CommandFactory, error::ErrorKind}; + use clap::{error::ErrorKind, CommandFactory}; use mockall::predicate::eq; use rstest::rstest; use serde_json::json; use tokio::sync::Mutex; use crate::{ - Cli, app::App, cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand}, models::{ - Serdeable, radarr_models::{ BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse, RadarrSerdeable, @@ -22,10 +20,12 @@ mod tests { BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse, SonarrSerdeable, }, + Serdeable, }, network::{ - MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent, sonarr_network::SonarrEvent, + radarr_network::RadarrEvent, sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent, }, + Cli, }; use pretty_assertions::assert_eq; @@ -55,6 +55,13 @@ mod tests { assert_ok!(&result); } + #[test] + fn test_lidarr_subcommand_delegates_to_lidarr() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]); + + assert_ok!(&result); + } + #[test] fn test_completions_requires_argument() { let result = Cli::command().try_get_matches_from(["managarr", "completions"]); @@ -174,4 +181,6 @@ mod tests { assert_ok!(&result); } + + // TODO: Implement test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler } diff --git a/src/cli/lidarr/delete_command_handler.rs b/src/cli/lidarr/delete_command_handler.rs new file mode 100644 index 0000000..131b5aa --- /dev/null +++ b/src/cli/lidarr/delete_command_handler.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + models::lidarr_models::DeleteArtistParams, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +use super::LidarrCommand; + +#[cfg(test)] +#[path = "delete_command_handler_tests.rs"] +mod delete_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrDeleteCommand { + #[command(about = "Delete an artist from your Lidarr library")] + Artist { + #[arg(long, help = "The ID of the artist to delete", required = true)] + artist_id: i64, + #[arg(long, help = "Delete the artist files from disk as well")] + delete_files_from_disk: bool, + #[arg(long, help = "Add a list exclusion for this artist")] + add_list_exclusion: bool, + }, +} + +impl From for Command { + fn from(value: LidarrDeleteCommand) -> Self { + Command::Lidarr(LidarrCommand::Delete(value)) + } +} + +pub(super) struct LidarrDeleteCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrDeleteCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + command: LidarrDeleteCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrDeleteCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrDeleteCommand::Artist { + artist_id, + delete_files_from_disk, + add_list_exclusion, + } => { + let delete_artist_params = DeleteArtistParams { + id: artist_id, + delete_files: delete_files_from_disk, + add_import_list_exclusion: add_list_exclusion, + }; + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteArtist(delete_artist_params).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/delete_command_handler_tests.rs b/src/cli/lidarr/delete_command_handler_tests.rs new file mode 100644 index 0000000..f36df29 --- /dev/null +++ b/src/cli/lidarr/delete_command_handler_tests.rs @@ -0,0 +1,145 @@ +#[cfg(test)] +mod tests { + use crate::{ + Cli, + cli::{ + Command, + lidarr::{LidarrCommand, delete_command_handler::LidarrDeleteCommand}, + }, + }; + use clap::{CommandFactory, Parser, error::ErrorKind}; + use pretty_assertions::assert_eq; + + #[test] + fn test_lidarr_delete_command_from() { + let command = LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: false, + add_list_exclusion: false, + }; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(LidarrCommand::Delete(command))); + } + + mod cli { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_delete_artist_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_artist_defaults() { + let expected_args = LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: false, + add_list_exclusion: false, + }; + + let result = + Cli::try_parse_from(["managarr", "lidarr", "delete", "artist", "--artist-id", "1"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_artist_all_args_defined() { + let expected_args = LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "artist", + "--artist-id", + "1", + "--delete-files-from-disk", + "--add-list-exclusion", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_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::delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler}, + }, + models::{ + Serdeable, + lidarr_models::{DeleteArtistParams, LidarrSerdeable}, + }, + network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, + }; + + #[tokio::test] + async fn test_handle_delete_artist_command() { + let expected_delete_artist_params = DeleteArtistParams { + id: 1, + delete_files: true, + add_import_list_exclusion: true, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteArtist(expected_delete_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 delete_artist_command = LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/lidarr_command_tests.rs b/src/cli/lidarr/lidarr_command_tests.rs index bf0ffaa..653f6d4 100644 --- a/src/cli/lidarr/lidarr_command_tests.rs +++ b/src/cli/lidarr/lidarr_command_tests.rs @@ -33,5 +33,92 @@ mod tests { assert_err!(&result); } + + #[test] + fn test_lidarr_delete_subcommand_requires_subcommand() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete"]); + + assert_err!(&result); + } + } + + 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::{ + LidarrCliHandler, LidarrCommand, + delete_command_handler::LidarrDeleteCommand, + list_command_handler::LidarrListCommand, + }, + }, + models::{ + Serdeable, + lidarr_models::{Artist, DeleteArtistParams, LidarrSerdeable}, + }, + network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, + }; + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() { + let expected_delete_artist_params = DeleteArtistParams { + id: 1, + delete_files: true, + add_import_list_exclusion: true, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteArtist(expected_delete_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 delete_artist_command = LidarrCommand::Delete(LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }); + + let result = LidarrCliHandler::with(&app_arc, delete_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_list_commands_to_the_list_command_handler() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::ListArtists.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Artists(vec![ + Artist::default(), + ]))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_artists_command = LidarrCommand::List(LidarrListCommand::Artists); + + let result = LidarrCliHandler::with(&app_arc, list_artists_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } } } diff --git a/src/cli/lidarr/mod.rs b/src/cli/lidarr/mod.rs index 99dab8b..1251bbb 100644 --- a/src/cli/lidarr/mod.rs +++ b/src/cli/lidarr/mod.rs @@ -2,16 +2,15 @@ use std::sync::Arc; use anyhow::Result; use clap::Subcommand; +use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler}; use list_command_handler::{LidarrListCommand, LidarrListCommandHandler}; use tokio::sync::Mutex; -use crate::{ - app::App, - network::NetworkTrait, -}; +use crate::{app::App, network::NetworkTrait}; use super::{CliCommandHandler, Command}; +mod delete_command_handler; mod list_command_handler; #[cfg(test)] @@ -20,6 +19,11 @@ mod lidarr_command_tests; #[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum LidarrCommand { + #[command( + subcommand, + about = "Commands to delete resources from your Lidarr instance" + )] + Delete(LidarrDeleteCommand), #[command( subcommand, about = "Commands to list attributes from your Lidarr instance" @@ -54,6 +58,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, ' async fn handle(self) -> Result { let result = match self.command { + LidarrCommand::Delete(delete_command) => { + LidarrDeleteCommandHandler::with(self.app, delete_command, self.network) + .handle() + .await? + } LidarrCommand::List(list_command) => { LidarrListCommandHandler::with(self.app, list_command, self.network) .handle() diff --git a/src/handlers/lidarr_handlers/library/delete_artist_handler.rs b/src/handlers/lidarr_handlers/library/delete_artist_handler.rs new file mode 100644 index 0000000..48c8251 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/delete_artist_handler.rs @@ -0,0 +1,149 @@ +use crate::models::lidarr_models::DeleteArtistParams; +use crate::network::lidarr_network::LidarrEvent; +use crate::{ + app::App, + event::Key, + handlers::{KeyEventHandler, handle_prompt_toggle}, + matches_key, + models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS}, +}; + +#[cfg(test)] +#[path = "delete_artist_handler_tests.rs"] +mod delete_artist_handler_tests; + +pub(in crate::handlers::lidarr_handlers) struct DeleteArtistHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl DeleteArtistHandler<'_, '_> { + fn build_delete_artist_params(&mut self) -> DeleteArtistParams { + let id = self.app.data.lidarr_data.artists.current_selection().id; + let delete_files = self.app.data.lidarr_data.delete_artist_files; + let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion; + self.app.data.lidarr_data.reset_delete_artist_preferences(); + + DeleteArtistParams { + id, + delete_files, + add_import_list_exclusion, + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + DELETE_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, + ) -> Self { + DeleteArtistHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + } + + fn handle_scroll_up(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + self.app.data.lidarr_data.selected_block.up(); + } + } + + fn handle_scroll_down(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + self.app.data.lidarr_data.selected_block.down(); + } + } + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + handle_prompt_toggle(self.app, self.key); + } + } + + fn handle_submit(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::DeleteArtistConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params())); + self.app.should_refresh = true; + } else { + self.app.data.lidarr_data.reset_delete_artist_preferences(); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::DeleteArtistToggleDeleteFile => { + self.app.data.lidarr_data.delete_artist_files = + !self.app.data.lidarr_data.delete_artist_files; + } + ActiveLidarrBlock::DeleteArtistToggleAddListExclusion => { + self.app.data.lidarr_data.add_import_list_exclusion = + !self.app.data.lidarr_data.add_import_list_exclusion; + } + _ => (), + } + } + } + + fn handle_esc(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.reset_delete_artist_preferences(); + self.app.data.lidarr_data.prompt_confirm = false; + } + } + + fn handle_char_key_event(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt + && self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::DeleteArtistConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteArtist(self.build_delete_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) -> crate::models::Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs b/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs new file mode 100644 index 0000000..64b50fe --- /dev/null +++ b/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs @@ -0,0 +1,410 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + 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::delete_artist_handler::DeleteArtistHandler; + use crate::models::lidarr_models::{Artist, DeleteArtistParams}; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS}; + + 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::DELETE_ARTIST_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_delete_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteArtistToggleDeleteFile + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteArtistConfirmPrompt + ); + } + } + + #[rstest] + fn test_delete_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.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteArtistToggleAddListExclusion + ); + } + } + + 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.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + + DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS; + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::assert_navigation_popped; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_delete_artist_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.delete_artist_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_artist_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_artist_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_artist_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + let expected_delete_artist_params = DeleteArtistParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1); + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::DeleteArtist(expected_delete_artist_params)) + ); + assert!(app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_artist_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_artist_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_artist_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::DeleteArtistPrompt.into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(app.data.lidarr_data.delete_artist_files); + assert!(app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_artist_toggle_delete_files_submit() { + let current_route = ActiveLidarrBlock::DeleteArtistPrompt.into(); + let mut app = App::test_default(); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.lidarr_data.delete_artist_files, true); + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.lidarr_data.delete_artist_files, false); + } + } + + mod test_handle_esc { + use super::*; + use crate::assert_navigation_popped; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_delete_artist_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_artist_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteArtistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_artist_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + } + + mod test_handle_key_char { + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS, + }, + network::lidarr_network::LidarrEvent, + }; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_delete_artist_confirm_prompt_prompt_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.delete_artist_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + let expected_delete_artist_params = DeleteArtistParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1); + + DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::DeleteArtist(expected_delete_artist_params)) + ); + assert!(app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_artist_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + } + + #[test] + fn test_delete_artist_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) { + assert!(DeleteArtistHandler::accepts(active_lidarr_block)); + } else { + assert!(!DeleteArtistHandler::accepts(active_lidarr_block)); + } + }); + } + + #[rstest] + fn test_delete_artist_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 = DeleteArtistHandler::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_build_delete_artist_params() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.data.lidarr_data.delete_artist_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + let expected_delete_artist_params = DeleteArtistParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + + let delete_artist_params = DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .build_delete_artist_params(); + + assert_eq!(delete_artist_params, expected_delete_artist_params); + assert!(!app.data.lidarr_data.delete_artist_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_artist_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.is_loading = true; + + let handler = DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_delete_artist_handler_ready_when_not_loading() { + let mut app = App::test_default(); + app.is_loading = false; + + let handler = DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/library/mod.rs b/src/handlers/lidarr_handlers/library/mod.rs index 348fc49..2845c6d 100644 --- a/src/handlers/lidarr_handlers/library/mod.rs +++ b/src/handlers/lidarr_handlers/library/mod.rs @@ -4,8 +4,11 @@ use crate::{ handlers::{KeyEventHandler, handle_clear_errors}, matches_key, models::{ + BlockSelectionState, lidarr_models::Artist, - servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS}, + servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS, + }, stateful_table::SortOption, }, }; @@ -13,6 +16,10 @@ use crate::{ use super::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +mod delete_artist_handler; + +pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler; + #[cfg(test)] #[path = "library_handler_tests.rs"] mod library_handler_tests; @@ -84,7 +91,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, ' fn handle_end(&mut self) {} - fn handle_delete(&mut self) {} + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::Artists { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + } + } fn handle_left_right_action(&mut self) { if self.active_lidarr_block == ActiveLidarrBlock::Artists { diff --git a/src/handlers/lidarr_handlers/mod.rs b/src/handlers/lidarr_handlers/mod.rs index 0002e73..6932737 100644 --- a/src/handlers/lidarr_handlers/mod.rs +++ b/src/handlers/lidarr_handlers/mod.rs @@ -1,4 +1,4 @@ -use library::LibraryHandler; +use library::{DeleteArtistHandler, LibraryHandler}; use crate::{ app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, @@ -22,6 +22,10 @@ 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(); } diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index 3ed7747..70ee24d 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -3,7 +3,7 @@ use derivative::Derivative; use enum_display_style_derive::EnumDisplayStyle; use serde::{Deserialize, Serialize}; use serde_json::{Number, Value}; -use strum::EnumIter; +use strum::{Display, EnumIter}; use super::{HorizontallyScrollableText, Serdeable}; use crate::serde_enum_from; @@ -45,7 +45,7 @@ pub struct Artist { Clone, Copy, Debug, - strum::Display, + Display, EnumDisplayStyle, )] #[serde(rename_all = "camelCase")] @@ -134,7 +134,7 @@ impl Eq for DownloadRecord {} Copy, Debug, EnumIter, - strum::Display, + Display, EnumDisplayStyle, )] #[serde(rename_all = "camelCase")] @@ -167,6 +167,14 @@ pub struct SystemStatus { pub start_time: DateTime, } +#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub struct DeleteArtistParams { + pub id: i64, + pub delete_files: bool, + pub add_import_list_exclusion: bool, +} + impl From for Serdeable { fn from(value: LidarrSerdeable) -> Serdeable { Serdeable::Lidarr(value) diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs index 2a311c3..144c6d8 100644 --- a/src/models/servarr_data/lidarr/lidarr_data.rs +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use strum::EnumIter; #[cfg(test)] use strum::{Display, EnumString}; - +use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES; use crate::models::{ Route, TabRoute, TabState, lidarr_models::{Artist, DownloadRecord}, @@ -17,7 +17,9 @@ use crate::network::lidarr_network::LidarrEvent; mod lidarr_data_tests; pub struct LidarrData<'a> { + pub add_import_list_exclusion: bool, pub artists: StatefulTable, + pub delete_artist_files: bool, pub disk_space_vec: Vec, pub downloads: StatefulTable, pub main_tabs: TabState, @@ -33,15 +35,18 @@ pub struct LidarrData<'a> { } impl LidarrData<'_> { - pub fn reset_sorting(&mut self) { - self.artists.sorting(vec![]); + pub fn reset_delete_artist_preferences(&mut self) { + self.delete_artist_files = false; + self.add_import_list_exclusion = false; } } impl<'a> Default for LidarrData<'a> { fn default() -> LidarrData<'a> { LidarrData { + add_import_list_exclusion: false, artists: StatefulTable::default(), + delete_artist_files: false, disk_space_vec: Vec::new(), downloads: StatefulTable::default(), metadata_profile_map: BiMap::new(), @@ -78,6 +83,8 @@ impl LidarrData<'_> { name: "Name", cmp_fn: Some(|a: &Artist, b: &Artist| a.artist_name.text.cmp(&b.artist_name.text)), }]); + 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())]); @@ -93,26 +100,16 @@ impl LidarrData<'_> { } } -use crate::app::context_clues::ContextClue; -use crate::app::key_binding::DEFAULT_KEYBINDINGS; - -pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 5] = [ - (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), - (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), - (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), - ( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc, - ), - (DEFAULT_KEYBINDINGS.esc, "cancel filter"), -]; - #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)] #[cfg_attr(test, derive(Display, EnumString))] pub enum ActiveLidarrBlock { #[default] Artists, ArtistsSortPrompt, + DeleteArtistPrompt, + DeleteArtistConfirmPrompt, + DeleteArtistToggleDeleteFile, + DeleteArtistToggleAddListExclusion, SearchArtists, SearchArtistsError, FilterArtists, @@ -128,6 +125,19 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 6] = [ ActiveLidarrBlock::FilterArtistsError, ]; +pub static DELETE_ARTIST_BLOCKS: [ActiveLidarrBlock; 4] = [ + ActiveLidarrBlock::DeleteArtistPrompt, + ActiveLidarrBlock::DeleteArtistConfirmPrompt, + ActiveLidarrBlock::DeleteArtistToggleDeleteFile, + ActiveLidarrBlock::DeleteArtistToggleAddListExclusion, +]; + +pub const DELETE_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::DeleteArtistToggleDeleteFile], + &[ActiveLidarrBlock::DeleteArtistToggleAddListExclusion], + &[ActiveLidarrBlock::DeleteArtistConfirmPrompt], +]; + 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 256b158..d8cb0ef 100644 --- a/src/models/servarr_data/lidarr/lidarr_data_tests.rs +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -2,7 +2,10 @@ mod tests { use pretty_assertions::assert_eq; - use crate::models::{servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, Route}; + use crate::models::{ + servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData}, + Route, + }; #[test] fn test_from_active_lidarr_block_to_route() { @@ -19,4 +22,18 @@ mod tests { Route::Lidarr(ActiveLidarrBlock::Artists, Some(ActiveLidarrBlock::Artists),) ); } + + #[test] + fn test_reset_delete_artist_preferences() { + let mut lidarr_data = LidarrData{ + delete_artist_files: true, + add_import_list_exclusion: true, + ..LidarrData::default() + }; + + lidarr_data.reset_delete_artist_preferences(); + + assert!(!lidarr_data.delete_artist_files); + assert!(!lidarr_data.add_import_list_exclusion); + } } diff --git a/src/models/servarr_data/mod.rs b/src/models/servarr_data/mod.rs index 256f0bc..bdf6f5b 100644 --- a/src/models/servarr_data/mod.rs +++ b/src/models/servarr_data/mod.rs @@ -1,9 +1,9 @@ use crate::models::Route; -pub mod lidarr; pub mod modals; pub mod radarr; pub mod sonarr; +pub mod lidarr; #[cfg(test)] pub(in crate::models::servarr_data) mod data_test_utils; diff --git a/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs b/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs new file mode 100644 index 0000000..fe31d05 --- /dev/null +++ b/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs @@ -0,0 +1,43 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::{DownloadsResponse, LidarrSerdeable}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[tokio::test] + async fn test_handle_get_downloads_event() { + let downloads_json = json!({ + "records": [{ + "title": "Test Album", + "status": "downloading", + "id": 1, + "size": 100.0, + "sizeleft": 50.0, + "indexer": "test-indexer" + }] + }); + let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(downloads_json) + .query("pageSize=500") + .build_for(LidarrEvent::GetDownloads(500)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetDownloads(500)) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else { + panic!("Expected DownloadsResponse"); + }; + + assert_eq!(downloads_response, response); + assert!(!app.lock().await.data.lidarr_data.downloads.is_empty()); + } +} diff --git a/src/network/lidarr_network/downloads/mod.rs b/src/network/lidarr_network/downloads/mod.rs new file mode 100644 index 0000000..3a0e918 --- /dev/null +++ b/src/network/lidarr_network/downloads/mod.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use log::info; + +use crate::models::lidarr_models::DownloadsResponse; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; + +#[cfg(test)] +#[path = "lidarr_downloads_network_tests.rs"] +mod lidarr_downloads_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn get_lidarr_downloads( + &mut self, + count: u64, + ) -> Result { + info!("Fetching Lidarr downloads"); + let event = LidarrEvent::GetDownloads(count); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("pageSize={count}")), + ) + .await; + + self + .handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| { + app + .data + .lidarr_data + .downloads + .set_items(queue_response.records); + }) + .await + } +} diff --git a/src/network/lidarr_network/library/lidarr_library_network_tests.rs b/src/network/lidarr_network/library/lidarr_library_network_tests.rs index 5f88749..9d6bc36 100644 --- a/src/network/lidarr_network/library/lidarr_library_network_tests.rs +++ b/src/network/lidarr_network/library/lidarr_library_network_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use crate::models::lidarr_models::{Artist, LidarrSerdeable}; + use crate::models::lidarr_models::{Artist, DeleteArtistParams, LidarrSerdeable}; use crate::network::lidarr_network::LidarrEvent; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use pretty_assertions::assert_eq; @@ -41,4 +41,29 @@ mod tests { assert_eq!(artists, response); assert!(!app.lock().await.data.lidarr_data.artists.is_empty()); } + + #[tokio::test] + async fn test_handle_delete_artist_event() { + let delete_artist_params = DeleteArtistParams { + id: 1, + delete_files: true, + add_import_list_exclusion: true, + }; + let (async_server, app, _server) = MockServarrApi::delete() + .path("/1") + .query("deleteFiles=true&addImportListExclusion=true") + .build_for(LidarrEvent::DeleteArtist(delete_artist_params.clone())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteArtist(delete_artist_params)) + .await + .is_ok() + ); + + async_server.assert_async().await; + } } diff --git a/src/network/lidarr_network/library/mod.rs b/src/network/lidarr_network/library/mod.rs index 77b8a80..a2c0923 100644 --- a/src/network/lidarr_network/library/mod.rs +++ b/src/network/lidarr_network/library/mod.rs @@ -1,7 +1,7 @@ use anyhow::Result; use log::info; -use crate::models::lidarr_models::Artist; +use crate::models::lidarr_models::{Artist, DeleteArtistParams}; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::Route; use crate::network::lidarr_network::LidarrEvent; @@ -12,6 +12,38 @@ use crate::network::{Network, RequestMethod}; mod lidarr_library_network_tests; impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn delete_artist( + &mut self, + delete_artist_params: DeleteArtistParams, + ) -> Result<()> { + let event = LidarrEvent::DeleteArtist(DeleteArtistParams::default()); + let DeleteArtistParams { + id, + delete_files, + add_import_list_exclusion, + } = delete_artist_params; + + info!( + "Deleting Lidarr artist with ID: {id} with deleteFiles={delete_files} and addImportListExclusion={add_import_list_exclusion}" + ); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{id}")), + Some(format!( + "deleteFiles={delete_files}&addImportListExclusion={add_import_list_exclusion}" + )), + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + pub(in crate::network::lidarr_network) async fn list_artists(&mut self) -> Result> { info!("Fetching Lidarr artists"); let event = LidarrEvent::ListArtists; diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index 53371ac..a435b34 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -1,8 +1,12 @@ #[cfg(test)] mod tests { - use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent}; - use pretty_assertions::assert_str_eq; + use crate::models::lidarr_models::{LidarrSerdeable, MetadataProfile}; + use crate::models::servarr_models::{QualityProfile, Tag}; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use crate::network::{lidarr_network::LidarrEvent, NetworkEvent, NetworkResource}; + use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; + use serde_json::json; #[rstest] #[case(LidarrEvent::GetDiskSpace, "/diskspace")] @@ -25,4 +29,109 @@ mod tests { NetworkEvent::from(LidarrEvent::HealthCheck) ); } + + #[tokio::test] + async fn test_handle_get_metadata_profiles_event() { + let metadata_profiles_json = json!([{ + "id": 1, + "name": "Standard" + }]); + let response: Vec = + serde_json::from_value(metadata_profiles_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(metadata_profiles_json) + .build_for(LidarrEvent::GetMetadataProfiles) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetMetadataProfiles) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else { + panic!("Expected MetadataProfiles"); + }; + + assert_eq!(metadata_profiles, response); + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .metadata_profile_map + .get_by_left(&1), + Some(&"Standard".to_owned()) + ); + } + + #[tokio::test] + async fn test_handle_get_quality_profiles_event() { + let quality_profiles_json = json!([{ + "id": 1, + "name": "Lossless" + }]); + let response: Vec = + serde_json::from_value(quality_profiles_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(quality_profiles_json) + .build_for(LidarrEvent::GetQualityProfiles) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetQualityProfiles) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else { + panic!("Expected QualityProfiles"); + }; + + assert_eq!(quality_profiles, response); + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .quality_profile_map + .get_by_left(&1), + Some(&"Lossless".to_owned()) + ); + } + + #[tokio::test] + async fn test_handle_get_tags_event() { + let tags_json = json!([{ + "id": 1, + "label": "usenet" + }]); + let response: Vec = serde_json::from_value(tags_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(tags_json) + .build_for(LidarrEvent::GetTags) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network.handle_lidarr_event(LidarrEvent::GetTags).await; + + mock.assert_async().await; + + let LidarrSerdeable::Tags(tags) = result.unwrap() else { + panic!("Expected Tags"); + }; + + assert_eq!(tags, response); + assert_eq!( + app.lock().await.data.lidarr_data.tags_map.get_by_left(&1), + Some(&"usenet".to_owned()) + ); + } } diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index 86981d7..abb5ec2 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -1,10 +1,14 @@ use anyhow::Result; +use log::info; use super::{NetworkEvent, NetworkResource}; -use crate::models::lidarr_models::LidarrSerdeable; -use crate::network::Network; +use crate::models::lidarr_models::{DeleteArtistParams, LidarrSerdeable, MetadataProfile}; +use crate::models::servarr_models::{QualityProfile, Tag}; +use crate::network::{Network, RequestMethod}; +mod downloads; mod library; +mod root_folders; mod system; #[cfg(test)] @@ -13,6 +17,7 @@ mod lidarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone)] pub enum LidarrEvent { + DeleteArtist(DeleteArtistParams), GetDiskSpace, GetDownloads(u64), GetMetadataProfiles, @@ -27,6 +32,7 @@ pub enum LidarrEvent { impl NetworkResource for LidarrEvent { fn resource(&self) -> &'static str { match &self { + LidarrEvent::DeleteArtist(_) | LidarrEvent::ListArtists => "/artist", LidarrEvent::GetDiskSpace => "/diskspace", LidarrEvent::GetDownloads(_) => "/queue", LidarrEvent::GetMetadataProfiles => "/metadataprofile", @@ -35,7 +41,6 @@ impl NetworkResource for LidarrEvent { LidarrEvent::GetStatus => "/system/status", LidarrEvent::GetTags => "/tag", LidarrEvent::HealthCheck => "/health", - LidarrEvent::ListArtists => "/artist", } } } @@ -52,6 +57,9 @@ impl Network<'_, '_> { lidarr_event: LidarrEvent, ) -> Result { match lidarr_event { + LidarrEvent::DeleteArtist(params) => { + self.delete_artist(params).await.map(LidarrSerdeable::from) + } LidarrEvent::GetDiskSpace => self .get_lidarr_diskspace() .await @@ -84,4 +92,58 @@ impl Network<'_, '_> { LidarrEvent::ListArtists => self.list_artists().await.map(LidarrSerdeable::from), } } + + async fn get_lidarr_metadata_profiles(&mut self) -> Result> { + info!("Fetching Lidarr metadata profiles"); + let event = LidarrEvent::GetMetadataProfiles; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |metadata_profiles, mut app| { + app.data.lidarr_data.metadata_profile_map = metadata_profiles + .into_iter() + .map(|profile| (profile.id, profile.name)) + .collect(); + }) + .await + } + + async fn get_lidarr_quality_profiles(&mut self) -> Result> { + info!("Fetching Lidarr quality profiles"); + let event = LidarrEvent::GetQualityProfiles; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |quality_profiles, mut app| { + app.data.lidarr_data.quality_profile_map = quality_profiles + .into_iter() + .map(|profile| (profile.id, profile.name)) + .collect(); + }) + .await + } + + async fn get_lidarr_tags(&mut self) -> Result> { + info!("Fetching Lidarr tags"); + let event = LidarrEvent::GetTags; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |tags_vec, mut app| { + app.data.lidarr_data.tags_map = tags_vec + .into_iter() + .map(|tag| (tag.id, tag.label)) + .collect(); + }) + .await + } } diff --git a/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs b/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs new file mode 100644 index 0000000..f8bfaf5 --- /dev/null +++ b/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs @@ -0,0 +1,39 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::LidarrSerdeable; + use crate::models::servarr_models::RootFolder; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[tokio::test] + async fn test_handle_get_root_folders_event() { + let root_folders_json = json!([{ + "id": 1, + "path": "/music", + "accessible": true, + "freeSpace": 50000000000i64 + }]); + let response: Vec = serde_json::from_value(root_folders_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(root_folders_json) + .build_for(LidarrEvent::GetRootFolders) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetRootFolders) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else { + panic!("Expected RootFolders"); + }; + + assert_eq!(root_folders, response); + assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty()); + } +} diff --git a/src/network/lidarr_network/root_folders/mod.rs b/src/network/lidarr_network/root_folders/mod.rs new file mode 100644 index 0000000..274c24c --- /dev/null +++ b/src/network/lidarr_network/root_folders/mod.rs @@ -0,0 +1,29 @@ +use anyhow::Result; +use log::info; + +use crate::models::servarr_models::RootFolder; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; + +#[cfg(test)] +#[path = "lidarr_root_folders_network_tests.rs"] +mod lidarr_root_folders_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders( + &mut self, + ) -> Result> { + info!("Fetching Lidarr root folders"); + let event = LidarrEvent::GetRootFolders; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |root_folders, mut app| { + app.data.lidarr_data.root_folders.set_items(root_folders); + }) + .await + } +} diff --git a/src/network/lidarr_network/system/lidarr_system_network_tests.rs b/src/network/lidarr_network/system/lidarr_system_network_tests.rs index 4695fc9..1457437 100644 --- a/src/network/lidarr_network/system/lidarr_system_network_tests.rs +++ b/src/network/lidarr_network/system/lidarr_system_network_tests.rs @@ -1,9 +1,7 @@ #[cfg(test)] mod tests { - use crate::models::lidarr_models::{ - DownloadsResponse, LidarrSerdeable, MetadataProfile, SystemStatus, - }; - use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag}; + use crate::models::lidarr_models::{LidarrSerdeable, SystemStatus}; + use crate::models::servarr_models::DiskSpace; use crate::network::lidarr_network::LidarrEvent; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use pretty_assertions::assert_eq; @@ -22,111 +20,6 @@ mod tests { mock.assert_async().await; } - #[tokio::test] - async fn test_handle_get_metadata_profiles_event() { - let metadata_profiles_json = json!([{ - "id": 1, - "name": "Standard" - }]); - let response: Vec = - serde_json::from_value(metadata_profiles_json.clone()).unwrap(); - let (mock, app, _server) = MockServarrApi::get() - .returns(metadata_profiles_json) - .build_for(LidarrEvent::GetMetadataProfiles) - .await; - app.lock().await.server_tabs.set_index(2); - let mut network = test_network(&app); - - let result = network - .handle_lidarr_event(LidarrEvent::GetMetadataProfiles) - .await; - - mock.assert_async().await; - - let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else { - panic!("Expected MetadataProfiles"); - }; - - assert_eq!(metadata_profiles, response); - assert_eq!( - app - .lock() - .await - .data - .lidarr_data - .metadata_profile_map - .get_by_left(&1), - Some(&"Standard".to_owned()) - ); - } - - #[tokio::test] - async fn test_handle_get_quality_profiles_event() { - let quality_profiles_json = json!([{ - "id": 1, - "name": "Lossless" - }]); - let response: Vec = - serde_json::from_value(quality_profiles_json.clone()).unwrap(); - let (mock, app, _server) = MockServarrApi::get() - .returns(quality_profiles_json) - .build_for(LidarrEvent::GetQualityProfiles) - .await; - app.lock().await.server_tabs.set_index(2); - let mut network = test_network(&app); - - let result = network - .handle_lidarr_event(LidarrEvent::GetQualityProfiles) - .await; - - mock.assert_async().await; - - let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else { - panic!("Expected QualityProfiles"); - }; - - assert_eq!(quality_profiles, response); - assert_eq!( - app - .lock() - .await - .data - .lidarr_data - .quality_profile_map - .get_by_left(&1), - Some(&"Lossless".to_owned()) - ); - } - - #[tokio::test] - async fn test_handle_get_tags_event() { - let tags_json = json!([{ - "id": 1, - "label": "usenet" - }]); - let response: Vec = serde_json::from_value(tags_json.clone()).unwrap(); - let (mock, app, _server) = MockServarrApi::get() - .returns(tags_json) - .build_for(LidarrEvent::GetTags) - .await; - app.lock().await.server_tabs.set_index(2); - let mut network = test_network(&app); - - let result = network.handle_lidarr_event(LidarrEvent::GetTags).await; - - mock.assert_async().await; - - let LidarrSerdeable::Tags(tags) = result.unwrap() else { - panic!("Expected Tags"); - }; - - assert_eq!(tags, response); - assert_eq!( - app.lock().await.data.lidarr_data.tags_map.get_by_left(&1), - Some(&"usenet".to_owned()) - ); - } - #[tokio::test] async fn test_handle_get_diskspace_event() { let diskspace_json = json!([{ @@ -153,71 +46,6 @@ mod tests { assert!(!app.lock().await.data.lidarr_data.disk_space_vec.is_empty()); } - #[tokio::test] - async fn test_handle_get_downloads_event() { - let downloads_json = json!({ - "records": [{ - "title": "Test Album", - "status": "downloading", - "id": 1, - "size": 100.0, - "sizeleft": 50.0, - "indexer": "test-indexer" - }] - }); - let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap(); - let (mock, app, _server) = MockServarrApi::get() - .returns(downloads_json) - .query("pageSize=500") - .build_for(LidarrEvent::GetDownloads(500)) - .await; - app.lock().await.server_tabs.set_index(2); - let mut network = test_network(&app); - - let result = network - .handle_lidarr_event(LidarrEvent::GetDownloads(500)) - .await; - - mock.assert_async().await; - - let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else { - panic!("Expected DownloadsResponse"); - }; - - assert_eq!(downloads_response, response); - assert!(!app.lock().await.data.lidarr_data.downloads.is_empty()); - } - - #[tokio::test] - async fn test_handle_get_root_folders_event() { - let root_folders_json = json!([{ - "id": 1, - "path": "/music", - "accessible": true, - "freeSpace": 50000000000i64 - }]); - let response: Vec = serde_json::from_value(root_folders_json.clone()).unwrap(); - let (mock, app, _server) = MockServarrApi::get() - .returns(root_folders_json) - .build_for(LidarrEvent::GetRootFolders) - .await; - app.lock().await.server_tabs.set_index(2); - let mut network = test_network(&app); - - let result = network - .handle_lidarr_event(LidarrEvent::GetRootFolders) - .await; - - mock.assert_async().await; - - let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else { - panic!("Expected RootFolders"); - }; - - assert_eq!(root_folders, response); - assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty()); - } - #[tokio::test] async fn test_handle_get_status_event() { let status_json = json!({ diff --git a/src/network/lidarr_network/system/mod.rs b/src/network/lidarr_network/system/mod.rs index 8cbedf2..2cb9196 100644 --- a/src/network/lidarr_network/system/mod.rs +++ b/src/network/lidarr_network/system/mod.rs @@ -1,8 +1,8 @@ use anyhow::Result; use log::info; -use crate::models::lidarr_models::{DownloadsResponse, MetadataProfile, SystemStatus}; -use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag}; +use crate::models::lidarr_models::SystemStatus; +use crate::models::servarr_models::DiskSpace; use crate::network::lidarr_network::LidarrEvent; use crate::network::{Network, RequestMethod}; @@ -24,64 +24,6 @@ impl Network<'_, '_> { .await } - pub(in crate::network::lidarr_network) async fn get_lidarr_metadata_profiles( - &mut self, - ) -> Result> { - info!("Fetching Lidarr metadata profiles"); - let event = LidarrEvent::GetMetadataProfiles; - - let request_props = self - .request_props_from(event, RequestMethod::Get, None::<()>, None, None) - .await; - - self - .handle_request::<(), Vec>(request_props, |metadata_profiles, mut app| { - app.data.lidarr_data.metadata_profile_map = metadata_profiles - .into_iter() - .map(|profile| (profile.id, profile.name)) - .collect(); - }) - .await - } - - pub(in crate::network::lidarr_network) async fn get_lidarr_quality_profiles( - &mut self, - ) -> Result> { - info!("Fetching Lidarr quality profiles"); - let event = LidarrEvent::GetQualityProfiles; - - let request_props = self - .request_props_from(event, RequestMethod::Get, None::<()>, None, None) - .await; - - self - .handle_request::<(), Vec>(request_props, |quality_profiles, mut app| { - app.data.lidarr_data.quality_profile_map = quality_profiles - .into_iter() - .map(|profile| (profile.id, profile.name)) - .collect(); - }) - .await - } - - pub(in crate::network::lidarr_network) async fn get_lidarr_tags(&mut self) -> Result> { - info!("Fetching Lidarr tags"); - let event = LidarrEvent::GetTags; - - let request_props = self - .request_props_from(event, RequestMethod::Get, None::<()>, None, None) - .await; - - self - .handle_request::<(), Vec>(request_props, |tags_vec, mut app| { - app.data.lidarr_data.tags_map = tags_vec - .into_iter() - .map(|tag| (tag.id, tag.label)) - .collect(); - }) - .await - } - pub(in crate::network::lidarr_network) async fn get_lidarr_diskspace( &mut self, ) -> Result> { @@ -99,51 +41,6 @@ impl Network<'_, '_> { .await } - pub(in crate::network::lidarr_network) async fn get_lidarr_downloads( - &mut self, - count: u64, - ) -> Result { - info!("Fetching Lidarr downloads"); - let event = LidarrEvent::GetDownloads(count); - - let request_props = self - .request_props_from( - event, - RequestMethod::Get, - None::<()>, - None, - Some(format!("pageSize={count}")), - ) - .await; - - self - .handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| { - app - .data - .lidarr_data - .downloads - .set_items(queue_response.records); - }) - .await - } - - pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders( - &mut self, - ) -> Result> { - info!("Fetching Lidarr root folders"); - let event = LidarrEvent::GetRootFolders; - - let request_props = self - .request_props_from(event, RequestMethod::Get, None::<()>, None, None) - .await; - - self - .handle_request::<(), Vec>(request_props, |root_folders, mut app| { - app.data.lidarr_data.root_folders.set_items(root_folders); - }) - .await - } - pub(in crate::network::lidarr_network) async fn get_lidarr_status( &mut self, ) -> Result { diff --git a/src/ui/lidarr_ui/library/delete_artist_ui.rs b/src/ui/lidarr_ui/library/delete_artist_ui.rs new file mode 100644 index 0000000..f6333f2 --- /dev/null +++ b/src/ui/lidarr_ui/library/delete_artist_ui.rs @@ -0,0 +1,57 @@ +use ratatui::Frame; +use ratatui::layout::Rect; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS}; +use crate::ui::DrawUi; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::popup::{Popup, Size}; + +#[cfg(test)] +#[path = "delete_artist_ui_tests.rs"] +mod delete_artist_ui_tests; + +pub(in crate::ui::lidarr_ui) struct DeleteArtistUi; + +impl DrawUi for DeleteArtistUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::DeleteArtistPrompt, _) + ) { + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let prompt = format!( + "Do you really want to delete the artist: \n{}?", + app.data.lidarr_data.artists.current_selection().artist_name.text + ); + let checkboxes = vec![ + Checkbox::new("Delete Artist Files") + .checked(app.data.lidarr_data.delete_artist_files) + .highlighted(selected_block == ActiveLidarrBlock::DeleteArtistToggleDeleteFile), + Checkbox::new("Add List Exclusion") + .checked(app.data.lidarr_data.add_import_list_exclusion) + .highlighted(selected_block == ActiveLidarrBlock::DeleteArtistToggleAddListExclusion), + ]; + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Artist") + .prompt(&prompt) + .checkboxes(checkboxes) + .yes_no_highlighted(selected_block == ActiveLidarrBlock::DeleteArtistConfirmPrompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + } +} diff --git a/src/ui/lidarr_ui/library/delete_artist_ui_tests.rs b/src/ui/lidarr_ui/library/delete_artist_ui_tests.rs new file mode 100644 index 0000000..9ccb8ba --- /dev/null +++ b/src/ui/lidarr_ui/library/delete_artist_ui_tests.rs @@ -0,0 +1,44 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::delete_artist_ui::DeleteArtistUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_delete_artist_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) { + assert!(DeleteArtistUi::accepts(active_lidarr_block.into())); + } else { + assert!(!DeleteArtistUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_delete_artist_ui_renders_delete_artist() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DeleteArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/library/library_ui_tests.rs b/src/ui/lidarr_ui/library/library_ui_tests.rs index 92bf3ce..9f7e931 100644 --- a/src/ui/lidarr_ui/library/library_ui_tests.rs +++ b/src/ui/lidarr_ui/library/library_ui_tests.rs @@ -2,19 +2,250 @@ mod tests { use strum::IntoEnumIterator; - use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS}; - use crate::models::Route; + use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, LIBRARY_BLOCKS, + }; + use crate::ui::lidarr_ui::library::{LibraryUi, decorate_artist_row_with_style}; + use crate::ui::styles::ManagarrStyle; use crate::ui::DrawUi; - use crate::ui::lidarr_ui::library::LibraryUi; + use pretty_assertions::assert_eq; + use ratatui::widgets::{Cell, Row}; #[test] fn test_library_ui_accepts() { - for lidarr_block in ActiveLidarrBlock::iter() { - if LIBRARY_BLOCKS.contains(&lidarr_block) { - assert!(LibraryUi::accepts(Route::Lidarr(lidarr_block, None))); + let mut library_ui_blocks = Vec::new(); + library_ui_blocks.extend(LIBRARY_BLOCKS); + library_ui_blocks.extend(DELETE_ARTIST_BLOCKS); + for active_lidarr_block in ActiveLidarrBlock::iter() { + if library_ui_blocks.contains(&active_lidarr_block) { + assert!(LibraryUi::accepts(active_lidarr_block.into())); } else { - assert!(!LibraryUi::accepts(Route::Lidarr(lidarr_block, None))); + assert!(!LibraryUi::accepts(active_lidarr_block.into())); } } } + + #[test] + fn test_decorate_row_with_style_unmonitored() { + let artist = Artist::default(); + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.unmonitored()); + } + + #[test] + fn test_decorate_row_with_style_downloaded_when_ended_and_all_tracks_present() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Ended, + statistics: Some(ArtistStatistics { + track_file_count: 10, + total_track_count: 10, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.downloaded()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_ended_and_tracks_are_missing() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Ended, + statistics: Some(ArtistStatistics { + track_file_count: 5, + total_track_count: 10, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_ended_and_no_statistics() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Ended, + statistics: None, + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_ended_and_total_track_count_is_zero() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Ended, + statistics: Some(ArtistStatistics { + track_file_count: 0, + total_track_count: 0, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_unreleased_when_continuing_and_all_tracks_present() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Continuing, + statistics: Some(ArtistStatistics { + track_file_count: 10, + total_track_count: 10, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.unreleased()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_continuing_and_tracks_are_missing() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Continuing, + statistics: Some(ArtistStatistics { + track_file_count: 5, + total_track_count: 10, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_continuing_and_no_statistics() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Continuing, + statistics: None, + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_defaults_to_indeterminate_for_deleted_status() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Deleted, + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + mod snapshot_tests { + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, + }; + use rstest::rstest; + + use crate::ui::lidarr_ui::library::LibraryUi; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + use crate::ui::DrawUi; + + #[rstest] + fn test_library_ui_renders( + #[values( + ActiveLidarrBlock::Artists, + ActiveLidarrBlock::ArtistsSortPrompt, + ActiveLidarrBlock::SearchArtists, + ActiveLidarrBlock::SearchArtistsError, + ActiveLidarrBlock::FilterArtists, + ActiveLidarrBlock::FilterArtistsError + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("lidarr_library_{active_lidarr_block}"), output); + } + + #[test] + fn test_library_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_library_ui_renders_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_library_ui_renders_delete_artist_over_library() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } } diff --git a/src/ui/lidarr_ui/library/mod.rs b/src/ui/lidarr_ui/library/mod.rs index a54c803..88e8a76 100644 --- a/src/ui/lidarr_ui/library/mod.rs +++ b/src/ui/lidarr_ui/library/mod.rs @@ -1,3 +1,4 @@ +use delete_artist_ui::DeleteArtistUi; use ratatui::{ Frame, layout::{Constraint, Rect}, @@ -20,6 +21,8 @@ use crate::{ }, }; +mod delete_artist_ui; + #[cfg(test)] #[path = "library_ui_tests.rs"] mod library_ui_tests; @@ -29,14 +32,19 @@ pub(super) struct LibraryUi; impl DrawUi for LibraryUi { fn accepts(route: Route) -> bool { if let Route::Lidarr(active_lidarr_block, _) = route { - return LIBRARY_BLOCKS.contains(&active_lidarr_block); + return DeleteArtistUi::accepts(route) || LIBRARY_BLOCKS.contains(&active_lidarr_block); } false } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); draw_library(f, app, area); + + if DeleteArtistUi::accepts(route) { + DeleteArtistUi::draw(f, app, area); + } } } diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_artist_ui__delete_artist_ui_tests__tests__snapshot_tests__delete_artist_ui_renders_delete_artist.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_artist_ui__delete_artist_ui_tests__tests__snapshot_tests__delete_artist_ui_renders_delete_artist.snap new file mode 100644 index 0000000..6329eb8 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_artist_ui__delete_artist_ui_tests__tests__snapshot_tests__delete_artist_ui_renders_delete_artist.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/library/delete_artist_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + + + ╭───────────────────── Delete Artist ─────────────────────╮ + │ Do you really want to delete the artist: │ + │ ? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Artist Files: │ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_delete_artist_over_library.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_delete_artist_over_library.snap new file mode 100644 index 0000000..83323e6 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_delete_artist_over_library.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Continuing 0 0.00 GB + + + + + + + + + + + + + + ╭───────────────────── Delete Artist ─────────────────────╮ + │ Do you really want to delete the artist: │ + │ ? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Artist Files: │ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap new file mode 100644 index 0000000..e17e3c0 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap new file mode 100644 index 0000000..b697d89 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_Artists.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_Artists.snap new file mode 100644 index 0000000..bf51331 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_Artists.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Continuing 0 0.00 GB diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_ArtistsSortPrompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_ArtistsSortPrompt.snap new file mode 100644 index 0000000..1204b85 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_ArtistsSortPrompt.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Continuing 0 0.00 GB + + + + + + + + + + + ╭───────────────────────────────╮ + │Name │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtists.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtists.snap new file mode 100644 index 0000000..3421078 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtists.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Continuing 0 0.00 GB + + + + + + + + + + + + + + + + + + + ╭───────────────── Filter ──────────────────╮ + │artist filter │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtistsError.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtistsError.snap new file mode 100644 index 0000000..913cbea --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtistsError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Continuing 0 0.00 GB + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │The given filter produced empty results│ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtists.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtists.snap new file mode 100644 index 0000000..42c7af9 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtists.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Continuing 0 0.00 GB + + + + + + + + + + + + + + + + + + + ╭───────────────── Search ──────────────────╮ + │artist search │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtistsError.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtistsError.snap new file mode 100644 index 0000000..e9ee85e --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtistsError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Continuing 0 0.00 GB + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │ No items found matching search │ + │ │ + ╰───────────────────────────────────────╯