From 09bee7473fa279df02eb21237c6022fbd58b4640 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 9 Jan 2026 16:33:32 -0700 Subject: [PATCH] feat: CLI support for deleting an album from Lidarr --- src/cli/lidarr/delete_command_handler.rs | 29 ++- .../lidarr/delete_command_handler_tests.rs | 97 ++++++++- src/cli/lidarr/lidarr_command_tests.rs | 4 +- .../library/artist_details_handler_tests.rs | 182 ++++++++-------- .../library/delete_artist_handler.rs | 6 +- .../library/delete_artist_handler_tests.rs | 8 +- src/models/lidarr_models.rs | 2 +- .../albums/lidarr_albums_network_tests.rs | 199 ++++++++++-------- .../lidarr_network/library/albums/mod.rs | 34 ++- .../artists/lidarr_artists_network_tests.rs | 6 +- .../lidarr_network/library/artists/mod.rs | 9 +- .../lidarr_network_test_utils.rs | 2 +- .../lidarr_network/lidarr_network_tests.rs | 18 +- src/network/lidarr_network/mod.rs | 18 +- 14 files changed, 402 insertions(+), 212 deletions(-) diff --git a/src/cli/lidarr/delete_command_handler.rs b/src/cli/lidarr/delete_command_handler.rs index 9a723cb..006d87c 100644 --- a/src/cli/lidarr/delete_command_handler.rs +++ b/src/cli/lidarr/delete_command_handler.rs @@ -7,7 +7,7 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - models::lidarr_models::DeleteArtistParams, + models::lidarr_models::DeleteParams, network::{NetworkTrait, lidarr_network::LidarrEvent}, }; @@ -19,6 +19,15 @@ mod delete_command_handler_tests; #[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum LidarrDeleteCommand { + #[command(about = "Delete an album from your Lidarr library")] + Album { + #[arg(long, help = "The ID of the album to delete", required = true)] + album_id: i64, + #[arg(long, help = "Delete the album files from disk as well")] + delete_files_from_disk: bool, + #[arg(long, help = "Add a list exclusion for this album")] + add_list_exclusion: bool, + }, #[command(about = "Delete an artist from your Lidarr library")] Artist { #[arg(long, help = "The ID of the artist to delete", required = true)] @@ -62,12 +71,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm async fn handle(self) -> Result { let result = match self.command { + LidarrDeleteCommand::Album { + album_id, + delete_files_from_disk, + add_list_exclusion, + } => { + let delete_album_params = DeleteParams { + id: album_id, + delete_files: delete_files_from_disk, + add_import_list_exclusion: add_list_exclusion, + }; + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteAlbum(delete_album_params).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrDeleteCommand::Artist { artist_id, delete_files_from_disk, add_list_exclusion, } => { - let delete_artist_params = DeleteArtistParams { + let delete_artist_params = DeleteParams { id: artist_id, delete_files: delete_files_from_disk, add_import_list_exclusion: add_list_exclusion, diff --git a/src/cli/lidarr/delete_command_handler_tests.rs b/src/cli/lidarr/delete_command_handler_tests.rs index 20f4460..1cc64f4 100644 --- a/src/cli/lidarr/delete_command_handler_tests.rs +++ b/src/cli/lidarr/delete_command_handler_tests.rs @@ -27,6 +27,65 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn test_delete_album_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "album"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_album_defaults() { + let expected_args = LidarrDeleteCommand::Album { + album_id: 1, + delete_files_from_disk: false, + add_list_exclusion: false, + }; + + let result = + Cli::try_parse_from(["managarr", "lidarr", "delete", "album", "--album-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_album_all_args_defined() { + let expected_args = LidarrDeleteCommand::Album { + album_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "album", + "--album-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); + } + #[test] fn test_delete_artist_requires_arguments() { let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]); @@ -128,14 +187,48 @@ mod tests { }, models::{ Serdeable, - lidarr_models::{DeleteArtistParams, LidarrSerdeable}, + lidarr_models::{DeleteParams, LidarrSerdeable}, }, network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, }; + #[tokio::test] + async fn test_handle_delete_album_command() { + let expected_delete_album_params = DeleteParams { + 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::DeleteAlbum(expected_delete_album_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_album_command = LidarrDeleteCommand::Album { + album_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_album_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_delete_artist_command() { - let expected_delete_artist_params = DeleteArtistParams { + let expected_delete_artist_params = DeleteParams { id: 1, delete_files: true, add_import_list_exclusion: true, diff --git a/src/cli/lidarr/lidarr_command_tests.rs b/src/cli/lidarr/lidarr_command_tests.rs index f814eec..320d560 100644 --- a/src/cli/lidarr/lidarr_command_tests.rs +++ b/src/cli/lidarr/lidarr_command_tests.rs @@ -147,7 +147,7 @@ mod tests { }, models::{ Serdeable, - lidarr_models::{Artist, DeleteArtistParams, LidarrSerdeable}, + lidarr_models::{Artist, DeleteParams, LidarrSerdeable}, }, network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, }; @@ -203,7 +203,7 @@ mod tests { #[tokio::test] async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() { - let expected_delete_artist_params = DeleteArtistParams { + let expected_delete_artist_params = DeleteParams { id: 1, delete_files: true, add_import_list_exclusion: true, diff --git a/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs b/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs index 2bd82d0..1900776 100644 --- a/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs +++ b/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs @@ -4,12 +4,12 @@ mod tests { use rstest::rstest; use strum::IntoEnumIterator; - use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; - use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; use crate::models::servarr_data::lidarr::lidarr_data::{ - ActiveLidarrBlock, ARTIST_DETAILS_BLOCKS, + ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, }; mod test_handle_left_right_action { @@ -17,64 +17,59 @@ mod tests { use crate::app::App; use crate::event::Key; - use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; #[rstest] fn test_left_right_prompt_toggle( #[values( - ActiveLidarrBlock::UpdateAndScanArtistPrompt, - ActiveLidarrBlock::AutomaticallySearchArtistPrompt, - )] active_lidarr_block: ActiveLidarrBlock, + ActiveLidarrBlock::UpdateAndScanArtistPrompt, + ActiveLidarrBlock::AutomaticallySearchArtistPrompt + )] + active_lidarr_block: ActiveLidarrBlock, #[values(Key::Left, Key::Right)] key: Key, ) { let mut app = App::test_default(); app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); app.push_navigation_stack(active_lidarr_block.into()); - ArtistDetailsHandler::new( - key, - &mut app, - active_lidarr_block, - None, - ) - .handle(); + ArtistDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle(); assert!(app.data.lidarr_data.prompt_confirm); - ArtistDetailsHandler::new( - key, - &mut app, - active_lidarr_block, - None, - ) - .handle(); + ArtistDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle(); assert!(!app.data.lidarr_data.prompt_confirm); } } mod test_handle_submit { - use rstest::rstest; - use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::assert_navigation_popped; use crate::event::Key; - use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; - use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist; use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist; + use rstest::rstest; const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; #[rstest] - #[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, LidarrEvent::TriggerAutomaticArtistSearch(1))] - #[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, LidarrEvent::UpdateAndScanArtist(1))] + #[case( + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + LidarrEvent::TriggerAutomaticArtistSearch(1) + )] + #[case( + ActiveLidarrBlock::UpdateAndScanArtistPrompt, + LidarrEvent::UpdateAndScanArtist(1) + )] fn test_artist_details_prompt_confirm_submit( #[case] prompt_block: ActiveLidarrBlock, - #[case] expected_action: LidarrEvent + #[case] expected_action: LidarrEvent, ) { let mut app = App::test_default(); app.data.lidarr_data.prompt_confirm = true; @@ -82,13 +77,7 @@ mod tests { app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); app.push_navigation_stack(prompt_block.into()); - ArtistDetailsHandler::new( - SUBMIT_KEY, - &mut app, - prompt_block, - None, - ) - .handle(); + ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); assert!(app.data.lidarr_data.prompt_confirm); assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); @@ -101,21 +90,16 @@ mod tests { #[rstest] fn test_artist_details_prompt_decline_submit( #[values( - ActiveLidarrBlock::AutomaticallySearchArtistPrompt, - ActiveLidarrBlock::UpdateAndScanArtistPrompt, - )] prompt_block: ActiveLidarrBlock + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + ActiveLidarrBlock::UpdateAndScanArtistPrompt + )] + prompt_block: ActiveLidarrBlock, ) { let mut app = App::test_default(); app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); app.push_navigation_stack(prompt_block.into()); - ArtistDetailsHandler::new( - SUBMIT_KEY, - &mut app, - prompt_block, - None, - ) - .handle(); + ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); assert!(!app.data.lidarr_data.prompt_confirm); assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); @@ -124,14 +108,14 @@ mod tests { } mod test_handle_esc { - use rstest::rstest; - use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::assert_navigation_popped; use crate::event::Key; - use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use rstest::rstest; const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; @@ -140,8 +124,9 @@ mod tests { #[values( ActiveLidarrBlock::AutomaticallySearchArtistPrompt, ActiveLidarrBlock::UpdateAndScanArtistPrompt - )] prompt_block: ActiveLidarrBlock, - #[values(true, false)] is_ready: bool + )] + prompt_block: ActiveLidarrBlock, + #[values(true, false)] is_ready: bool, ) { let mut app = App::test_default(); app.is_loading = is_ready; @@ -157,21 +142,23 @@ mod tests { } mod test_handle_char_key_event { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; + use crate::models::lidarr_models::Artist; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS, + }; + use crate::network::lidarr_network::LidarrEvent; + use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped}; use pretty_assertions::assert_eq; use rstest::rstest; - use crate::app::key_binding::DEFAULT_KEYBINDINGS; - use crate::app::App; - use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped}; - use crate::assert_navigation_pushed; - use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; - use crate::handlers::KeyEventHandler; - use crate::models::lidarr_models::{Artist}; - use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS}; - use crate::network::lidarr_network::LidarrEvent; #[rstest] fn test_artist_details_edit_key( - #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default_fully_populated(); app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); @@ -183,7 +170,7 @@ mod tests { active_lidarr_block, None, ) - .handle(); + .handle(); assert_navigation_pushed!( app, @@ -203,7 +190,7 @@ mod tests { #[rstest] fn test_artist_details_edit_key_no_op_when_not_ready( - #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default(); app.is_loading = true; @@ -216,7 +203,7 @@ mod tests { active_lidarr_block, None, ) - .handle(); + .handle(); assert_eq!(app.get_current_route(), active_lidarr_block.into()); assert_modal_absent!(app.data.lidarr_data.edit_artist_modal); @@ -235,9 +222,12 @@ mod tests { ActiveLidarrBlock::ArtistDetails, None, ) - .handle(); + .handle(); - assert_eq!(app.get_current_route(), ActiveLidarrBlock::ArtistDetails.into()); + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ArtistDetails.into() + ); assert!(app.data.lidarr_data.prompt_confirm); assert!(app.is_routing); assert_eq!( @@ -259,9 +249,12 @@ mod tests { ActiveLidarrBlock::ArtistDetails, None, ) - .handle(); + .handle(); - assert_eq!(app.get_current_route(), ActiveLidarrBlock::ArtistDetails.into()); + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ArtistDetails.into() + ); assert!(!app.data.lidarr_data.prompt_confirm); assert_none!(app.data.lidarr_data.prompt_confirm_action); } @@ -281,7 +274,7 @@ mod tests { ActiveLidarrBlock::ArtistDetails, None, ) - .handle(); + .handle(); assert!(!app.data.lidarr_data.prompt_confirm); assert!(app.data.lidarr_data.prompt_confirm_action.is_none()); @@ -289,7 +282,7 @@ mod tests { #[rstest] fn test_artist_details_auto_search_key( - #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default_fully_populated(); app.push_navigation_stack(active_lidarr_block.into()); @@ -300,7 +293,7 @@ mod tests { active_lidarr_block, None, ) - .handle(); + .handle(); assert_navigation_pushed!( app, @@ -310,7 +303,7 @@ mod tests { #[rstest] fn test_artist_details_auto_search_key_no_op_when_not_ready( - #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default_fully_populated(); app.is_loading = true; @@ -322,14 +315,14 @@ mod tests { active_lidarr_block, None, ) - .handle(); + .handle(); assert_eq!(app.get_current_route(), active_lidarr_block.into()); } #[rstest] fn test_artist_details_update_key( - #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default_fully_populated(); app.push_navigation_stack(active_lidarr_block.into()); @@ -340,14 +333,14 @@ mod tests { active_lidarr_block, None, ) - .handle(); + .handle(); assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAndScanArtistPrompt.into()); } #[rstest] fn test_artist_details_update_key_no_op_when_not_ready( - #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default_fully_populated(); app.is_loading = true; @@ -359,14 +352,14 @@ mod tests { active_lidarr_block, None, ) - .handle(); + .handle(); assert_eq!(app.get_current_route(), active_lidarr_block.into()); } #[rstest] fn test_artist_details_refresh_key( - #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default_fully_populated(); app.is_routing = false; @@ -381,16 +374,13 @@ mod tests { ) .handle(); - assert_navigation_pushed!( - app, - active_lidarr_block.into() - ); + assert_navigation_pushed!(app, active_lidarr_block.into()); assert!(app.is_routing); } #[rstest] fn test_artist_details_refresh_key_no_op_when_not_ready( - #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default(); app.is_loading = true; @@ -403,12 +393,9 @@ mod tests { active_lidarr_block, None, ) - .handle(); + .handle(); - assert_eq!( - app.get_current_route(), - active_lidarr_block.into() - ); + assert_eq!(app.get_current_route(), active_lidarr_block.into()); assert!(!app.is_routing); } @@ -424,8 +411,7 @@ mod tests { fn test_artist_details_prompt_confirm_key( #[case] prompt_block: ActiveLidarrBlock, #[case] expected_action: LidarrEvent, - #[values(ActiveLidarrBlock::ArtistDetails)] - active_lidarr_block: ActiveLidarrBlock, + #[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock, ) { let mut app = App::test_default_fully_populated(); app.push_navigation_stack(active_lidarr_block.into()); @@ -463,8 +449,13 @@ mod tests { fn test_extract_artist_id() { let mut app = App::test_default_fully_populated(); - let artist_id = ArtistDetailsHandler::new(DEFAULT_KEYBINDINGS.esc.key, - &mut app, ActiveLidarrBlock::ArtistDetails, None).extract_artist_id(); + let artist_id = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ) + .extract_artist_id(); assert_eq!(artist_id, 1); } @@ -473,8 +464,13 @@ mod tests { fn test_extract_album_id() { let mut app = App::test_default_fully_populated(); - let album_id = ArtistDetailsHandler::new(DEFAULT_KEYBINDINGS.esc.key, - &mut app, ActiveLidarrBlock::ArtistDetails, None).extract_album_id(); + let album_id = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ) + .extract_album_id(); assert_eq!(album_id, 1); } diff --git a/src/handlers/lidarr_handlers/library/delete_artist_handler.rs b/src/handlers/lidarr_handlers/library/delete_artist_handler.rs index 89fc39d..95d00d3 100644 --- a/src/handlers/lidarr_handlers/library/delete_artist_handler.rs +++ b/src/handlers/lidarr_handlers/library/delete_artist_handler.rs @@ -1,5 +1,5 @@ use crate::models::Route; -use crate::models::lidarr_models::DeleteArtistParams; +use crate::models::lidarr_models::DeleteParams; use crate::network::lidarr_network::LidarrEvent; use crate::{ app::App, @@ -21,13 +21,13 @@ pub(in crate::handlers::lidarr_handlers) struct DeleteArtistHandler<'a, 'b> { } impl DeleteArtistHandler<'_, '_> { - fn build_delete_artist_params(&mut self) -> DeleteArtistParams { + fn build_delete_artist_params(&mut self) -> DeleteParams { 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 { + DeleteParams { id, delete_files, add_import_list_exclusion, diff --git a/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs b/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs index 64b50fe..3fccce0 100644 --- a/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs +++ b/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs @@ -9,7 +9,7 @@ mod tests { 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::lidarr_models::{Artist, DeleteParams}; use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS}; mod test_handle_scroll_up_and_down { @@ -137,7 +137,7 @@ mod tests { .lidarr_data .artists .set_items(vec![Artist::default()]); - let expected_delete_artist_params = DeleteArtistParams { + let expected_delete_artist_params = DeleteParams { id: 0, delete_files: true, add_import_list_exclusion: true, @@ -286,7 +286,7 @@ mod tests { .lidarr_data .artists .set_items(vec![Artist::default()]); - let expected_delete_artist_params = DeleteArtistParams { + let expected_delete_artist_params = DeleteParams { id: 0, delete_files: true, add_import_list_exclusion: true, @@ -359,7 +359,7 @@ mod tests { .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 { + let expected_delete_artist_params = DeleteParams { id: 0, delete_files: true, add_import_list_exclusion: true, diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index 848ef16..83e380d 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -255,7 +255,7 @@ pub struct LidarrCommandBody { #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[serde(rename_all = "lowercase")] -pub struct DeleteArtistParams { +pub struct DeleteParams { pub id: i64, pub delete_files: bool, pub add_import_list_exclusion: bool, diff --git a/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs b/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs index 61a8652..e36ff6e 100644 --- a/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs +++ b/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs @@ -1,106 +1,131 @@ #[cfg(test)] mod tests { - use mockito::Matcher; - use pretty_assertions::assert_eq; - use serde_json::{json, Value}; - use crate::models::lidarr_models::{Album, LidarrSerdeable}; - use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ALBUM_JSON}; - use crate::network::lidarr_network::LidarrEvent; - use crate::network::network_tests::test_utils::{test_network, MockServarrApi}; + use crate::models::lidarr_models::{Album, DeleteParams, LidarrSerdeable}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::ALBUM_JSON; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use mockito::Matcher; + use pretty_assertions::assert_eq; + use serde_json::{Value, json}; - #[tokio::test] - async fn test_handle_get_albums_event() { - let albums_json = json!([{ + #[tokio::test] + async fn test_handle_get_albums_event() { + let albums_json = json!([{ "id": 1, "title": "Test Album", - "foreignAlbumId": "test-foreign-album-id", - "monitored": true, - "anyReleaseOk": true, - "profileId": 1, - "duration": 180, - "albumType": "Album", - "genres": ["Classical"], - "ratings": {"votes": 15, "value": 8.4}, - "releaseDate": "2023-01-01T00:00:00Z", - "statistics": { - "trackFileCount": 10, - "trackCount": 10, - "totalTrackCount": 10, - "sizeOnDisk": 1024, - "percentOfTracks": 99.9 - } + "foreignAlbumId": "test-foreign-album-id", + "monitored": true, + "anyReleaseOk": true, + "profileId": 1, + "duration": 180, + "albumType": "Album", + "genres": ["Classical"], + "ratings": {"votes": 15, "value": 8.4}, + "releaseDate": "2023-01-01T00:00:00Z", + "statistics": { + "trackFileCount": 10, + "trackCount": 10, + "totalTrackCount": 10, + "sizeOnDisk": 1024, + "percentOfTracks": 99.9 + } }]); - let response: Vec = serde_json::from_value(albums_json.clone()).unwrap(); - let (mock, app, _server) = MockServarrApi::get() - .returns(albums_json) - .query("artistId=1") - .build_for(LidarrEvent::GetAlbums(1)) - .await; - app.lock().await.server_tabs.set_index(2); - let mut network = test_network(&app); + let response: Vec = serde_json::from_value(albums_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(albums_json) + .query("artistId=1") + .build_for(LidarrEvent::GetAlbums(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); - let result = network.handle_lidarr_event(LidarrEvent::GetAlbums(1)).await; + let result = network.handle_lidarr_event(LidarrEvent::GetAlbums(1)).await; - mock.assert_async().await; + mock.assert_async().await; - let LidarrSerdeable::Albums(albums) = result.unwrap() else { - panic!("Expected Albums"); - }; + let LidarrSerdeable::Albums(albums) = result.unwrap() else { + panic!("Expected Albums"); + }; - assert_eq!(albums, response); - assert!(!app.lock().await.data.lidarr_data.albums.is_empty()); - } + assert_eq!(albums, response); + assert!(!app.lock().await.data.lidarr_data.albums.is_empty()); + } - #[tokio::test] - async fn test_handle_toggle_album_monitoring_event() { - let mut expected_body: Value = serde_json::from_str(ALBUM_JSON).unwrap(); - *expected_body.get_mut("monitored").unwrap() = json!(false); - let (get_mock, app, mut server) = MockServarrApi::get() - .returns(serde_json::from_str(ALBUM_JSON).unwrap()) - .path("/1") - .build_for(LidarrEvent::GetAlbums(1)) - .await; - let put_mock = server - .mock("PUT", "/api/v1/album/1") - .match_body(Matcher::Json(expected_body)) - .match_header("X-Api-Key", "test1234") - .with_status(202) - .create_async() - .await; - app.lock().await.server_tabs.set_index(2); - let mut network = test_network(&app); + #[tokio::test] + async fn test_handle_delete_album_event() { + let delete_album_params = DeleteParams { + 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::DeleteAlbum(delete_album_params.clone())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); - assert_ok!( - network - .handle_lidarr_event(LidarrEvent::ToggleAlbumMonitoring(1)) - .await - ); + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteAlbum(delete_album_params)) + .await + .is_ok() + ); - get_mock.assert_async().await; - put_mock.assert_async().await; - } + async_server.assert_async().await; + } - #[tokio::test] - async fn test_handle_get_album_details_event() { - let expected_album: Album = serde_json::from_str(ALBUM_JSON).unwrap(); - let (mock, app, _server) = MockServarrApi::get() - .returns(serde_json::from_str(ALBUM_JSON).unwrap()) - .path("/1") - .build_for(LidarrEvent::GetAlbumDetails(1)) - .await; - app.lock().await.server_tabs.set_index(2); - let mut network = test_network(&app); + #[tokio::test] + async fn test_handle_toggle_album_monitoring_event() { + let mut expected_body: Value = serde_json::from_str(ALBUM_JSON).unwrap(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + let (get_mock, app, mut server) = MockServarrApi::get() + .returns(serde_json::from_str(ALBUM_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetAlbums(1)) + .await; + let put_mock = server + .mock("PUT", "/api/v1/album/1") + .match_body(Matcher::Json(expected_body)) + .match_header("X-Api-Key", "test1234") + .with_status(202) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); - let result = network - .handle_lidarr_event(LidarrEvent::GetAlbumDetails(1)) - .await; + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::ToggleAlbumMonitoring(1)) + .await + ); - mock.assert_async().await; + get_mock.assert_async().await; + put_mock.assert_async().await; + } - let LidarrSerdeable::Album(album) = result.unwrap() else { - panic!("Expected Album"); - }; + #[tokio::test] + async fn test_handle_get_album_details_event() { + let expected_album: Album = serde_json::from_str(ALBUM_JSON).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(serde_json::from_str(ALBUM_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetAlbumDetails(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); - assert_eq!(album, expected_album); - } + let result = network + .handle_lidarr_event(LidarrEvent::GetAlbumDetails(1)) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::Album(album) = result.unwrap() else { + panic!("Expected Album"); + }; + + assert_eq!(album, expected_album); + } } diff --git a/src/network/lidarr_network/library/albums/mod.rs b/src/network/lidarr_network/library/albums/mod.rs index 63c41fa..b8becdd 100644 --- a/src/network/lidarr_network/library/albums/mod.rs +++ b/src/network/lidarr_network/library/albums/mod.rs @@ -1,4 +1,4 @@ -use crate::models::lidarr_models::{Album}; +use crate::models::lidarr_models::{Album, DeleteParams}; use crate::network::lidarr_network::LidarrEvent; use crate::network::{Network, RequestMethod}; use anyhow::Result; @@ -57,6 +57,38 @@ impl Network<'_, '_> { .await } + pub(in crate::network::lidarr_network) async fn delete_album( + &mut self, + delete_album_params: DeleteParams, + ) -> Result<()> { + let event = LidarrEvent::DeleteAlbum(DeleteParams::default()); + let DeleteParams { + id, + delete_files, + add_import_list_exclusion, + } = delete_album_params; + + info!( + "Deleting Lidarr album 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 toggle_album_monitoring( &mut self, album_id: i64, diff --git a/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs b/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs index 5b3e45f..319780d 100644 --- a/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs +++ b/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs @@ -1,8 +1,8 @@ #[cfg(test)] mod tests { use crate::models::lidarr_models::{ - AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteArtistParams, - EditArtistParams, LidarrSerdeable, MonitorType, NewItemMonitorType, + AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, + LidarrSerdeable, MonitorType, NewItemMonitorType, }; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::network::NetworkResource; @@ -54,7 +54,7 @@ mod tests { #[tokio::test] async fn test_handle_delete_artist_event() { - let delete_artist_params = DeleteArtistParams { + let delete_artist_params = DeleteParams { id: 1, delete_files: true, add_import_list_exclusion: true, diff --git a/src/network/lidarr_network/library/artists/mod.rs b/src/network/lidarr_network/library/artists/mod.rs index f7bc5ec..9937abe 100644 --- a/src/network/lidarr_network/library/artists/mod.rs +++ b/src/network/lidarr_network/library/artists/mod.rs @@ -4,8 +4,7 @@ use serde_json::{Value, json}; use crate::models::Route; use crate::models::lidarr_models::{ - AddArtistBody, AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams, - LidarrCommandBody, + AddArtistBody, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, LidarrCommandBody, }; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::stateful_table::StatefulTable; @@ -20,10 +19,10 @@ mod lidarr_artists_network_tests; impl Network<'_, '_> { pub(in crate::network::lidarr_network) async fn delete_artist( &mut self, - delete_artist_params: DeleteArtistParams, + delete_artist_params: DeleteParams, ) -> Result<()> { - let event = LidarrEvent::DeleteArtist(DeleteArtistParams::default()); - let DeleteArtistParams { + let event = LidarrEvent::DeleteArtist(DeleteParams::default()); + let DeleteParams { id, delete_files, add_import_list_exclusion, diff --git a/src/network/lidarr_network/lidarr_network_test_utils.rs b/src/network/lidarr_network/lidarr_network_test_utils.rs index a5e514d..213ec49 100644 --- a/src/network/lidarr_network/lidarr_network_test_utils.rs +++ b/src/network/lidarr_network/lidarr_network_test_utils.rs @@ -50,7 +50,7 @@ pub mod test_utils { "percentOfTracks": 99.9 } }"#; - + pub const ALBUM_JSON: &str = r#"{ "id": 1, "title": "Test Album", diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index c9f1e7a..c5292f3 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -1,7 +1,9 @@ #[cfg(test)] mod tests { use crate::app::App; - use crate::models::lidarr_models::{AddArtistBody, LidarrSerdeable, MetadataProfile}; + use crate::models::lidarr_models::{ + AddArtistBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile, + }; use crate::models::servarr_data::lidarr::modals::EditArtistModal; use crate::models::servarr_models::{QualityProfile, Tag}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; @@ -19,7 +21,9 @@ mod tests { LidarrEvent::GetArtistDetails(0), LidarrEvent::ListArtists, LidarrEvent::AddArtist(AddArtistBody::default()), - LidarrEvent::ToggleArtistMonitoring(0) + LidarrEvent::ToggleArtistMonitoring(0), + LidarrEvent::DeleteArtist(DeleteParams::default()), + LidarrEvent::EditArtist(EditArtistParams::default()) )] event: LidarrEvent, ) { @@ -58,8 +62,14 @@ mod tests { } #[rstest] - fn test_resource_albums( - #[values(LidarrEvent::GetAlbums(0), LidarrEvent::ToggleAlbumMonitoring(0), LidarrEvent::GetAlbumDetails(0))] event: LidarrEvent, + fn test_resource_album( + #[values( + LidarrEvent::GetAlbums(0), + LidarrEvent::ToggleAlbumMonitoring(0), + LidarrEvent::GetAlbumDetails(0), + LidarrEvent::DeleteAlbum(DeleteParams::default()) + )] + event: LidarrEvent, ) { assert_str_eq!(event.resource(), "/album"); } diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index 518c357..d2be3cb 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -3,7 +3,7 @@ use log::info; use super::{NetworkEvent, NetworkResource}; use crate::models::lidarr_models::{ - AddArtistBody, DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile, + AddArtistBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile, }; use crate::models::servarr_models::{QualityProfile, Tag}; use crate::network::{Network, RequestMethod}; @@ -25,7 +25,8 @@ pub mod lidarr_network_test_utils; pub enum LidarrEvent { AddArtist(AddArtistBody), AddTag(String), - DeleteArtist(DeleteArtistParams), + DeleteAlbum(DeleteParams), + DeleteArtist(DeleteParams), DeleteTag(i64), EditArtist(EditArtistParams), GetAlbums(i64), @@ -60,7 +61,10 @@ impl NetworkResource for LidarrEvent { | LidarrEvent::ListArtists | LidarrEvent::AddArtist(_) | LidarrEvent::ToggleArtistMonitoring(_) => "/artist", - LidarrEvent::GetAlbums(_) | LidarrEvent::ToggleAlbumMonitoring(_) | LidarrEvent::GetAlbumDetails(_) => "/album", + LidarrEvent::GetAlbums(_) + | LidarrEvent::ToggleAlbumMonitoring(_) + | LidarrEvent::GetAlbumDetails(_) + | LidarrEvent::DeleteAlbum(_) => "/album", LidarrEvent::GetDiskSpace => "/diskspace", LidarrEvent::GetDownloads(_) => "/queue", LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host", @@ -90,6 +94,9 @@ impl Network<'_, '_> { ) -> Result { match lidarr_event { LidarrEvent::AddTag(tag) => self.add_lidarr_tag(tag).await.map(LidarrSerdeable::from), + LidarrEvent::DeleteAlbum(params) => { + self.delete_album(params).await.map(LidarrSerdeable::from) + } LidarrEvent::DeleteArtist(params) => { self.delete_artist(params).await.map(LidarrSerdeable::from) } @@ -104,7 +111,10 @@ impl Network<'_, '_> { .get_artist_details(artist_id) .await .map(LidarrSerdeable::from), - LidarrEvent::GetAlbumDetails(album_id) => self.get_album_details(album_id).await.map(LidarrSerdeable::from), + LidarrEvent::GetAlbumDetails(album_id) => self + .get_album_details(album_id) + .await + .map(LidarrSerdeable::from), LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from), LidarrEvent::GetDownloads(count) => self .get_lidarr_downloads(count)