feat: Full support for deleting an artist via CLI and TUI

This commit is contained in:
2026-01-05 15:44:51 -07:00
parent bc3aeefa6e
commit 6771a0ab38
43 changed files with 1995 additions and 332 deletions
+13 -4
View File
@@ -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
}
+80
View File
@@ -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<LidarrDeleteCommand> for Command {
fn from(value: LidarrDeleteCommand) -> Self {
Command::Lidarr(LidarrCommand::Delete(value))
}
}
pub(super) struct LidarrDeleteCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrDeleteCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
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)
}
}
@@ -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::<NetworkEvent>(
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);
}
}
}
+87
View File
@@ -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::<NetworkEvent>(
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::<NetworkEvent>(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);
}
}
}
+13 -4
View File
@@ -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<String> {
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()