From 8002a5aa1eb691c642719156034d8da0d84315c0 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 22 Nov 2024 19:53:54 -0700 Subject: [PATCH] feat(cli): Support for downloading a Series release in Sonarr --- src/cli/sonarr/download_command_handler.rs | 90 ++++++++++ .../sonarr/download_command_handler_tests.rs | 166 ++++++++++++++++++ src/cli/sonarr/mod.rs | 12 ++ src/cli/sonarr/sonarr_command_tests.rs | 44 ++++- 4 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 src/cli/sonarr/download_command_handler.rs create mode 100644 src/cli/sonarr/download_command_handler_tests.rs diff --git a/src/cli/sonarr/download_command_handler.rs b/src/cli/sonarr/download_command_handler.rs new file mode 100644 index 0000000..3fa624f --- /dev/null +++ b/src/cli/sonarr/download_command_handler.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + models::sonarr_models::SonarrReleaseDownloadBody, + network::{sonarr_network::SonarrEvent, NetworkTrait}, +}; + +use super::SonarrCommand; + +#[cfg(test)] +#[path = "download_command_handler_tests.rs"] +mod download_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum SonarrDownloadCommand { + #[command(about = "Manually download the given series release for the specified series ID")] + Series { + #[arg(long, help = "The GUID of the release to download", required = true)] + guid: String, + #[arg( + long, + help = "The indexer ID to download the release from", + required = true + )] + indexer_id: i64, + #[arg( + long, + help = "The series ID that the release is associated with", + required = true + )] + series_id: i64, + }, +} + +impl From for Command { + fn from(value: SonarrDownloadCommand) -> Self { + Command::Sonarr(SonarrCommand::Download(value)) + } +} + +pub(super) struct SonarrDownloadCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: SonarrDownloadCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDownloadCommand> + for SonarrDownloadCommandHandler<'a, 'b> +{ + fn with( + _app: &'a Arc>>, + command: SonarrDownloadCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + SonarrDownloadCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> anyhow::Result { + let result = match self.command { + SonarrDownloadCommand::Series { + guid, + indexer_id, + series_id, + } => { + let params = SonarrReleaseDownloadBody { + guid, + indexer_id, + series_id: Some(series_id), + ..SonarrReleaseDownloadBody::default() + }; + let resp = self + .network + .handle_network_event(SonarrEvent::DownloadRelease(params).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/sonarr/download_command_handler_tests.rs b/src/cli/sonarr/download_command_handler_tests.rs new file mode 100644 index 0000000..870b109 --- /dev/null +++ b/src/cli/sonarr/download_command_handler_tests.rs @@ -0,0 +1,166 @@ +#[cfg(test)] +mod tests { + use crate::{ + cli::{ + sonarr::{download_command_handler::SonarrDownloadCommand, SonarrCommand}, + Command, + }, + Cli, + }; + use clap::CommandFactory; + use pretty_assertions::assert_eq; + + #[test] + fn test_sonarr_download_command_from() { + let command = SonarrDownloadCommand::Series { + guid: "Test".to_owned(), + indexer_id: 1, + series_id: 1, + }; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Sonarr(SonarrCommand::Download(command))); + } + + mod cli { + use super::*; + use clap::error::ErrorKind; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_download_series_requires_series_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "download", + "series", + "--indexer-id", + "1", + "--guid", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[rstest] + fn test_download_series_requires_guid() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "download", + "series", + "--indexer-id", + "1", + "--series-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[rstest] + fn test_download_series_requires_indexer_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "download", + "series", + "--guid", + "1", + "--series-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_download_series_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "download", + "series", + "--guid", + "1", + "--series-id", + "1", + "--indexer-id", + "1", + ]); + + assert!(result.is_ok()); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{ + app::App, + cli::{ + sonarr::download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler}, + CliCommandHandler, + }, + models::{ + sonarr_models::{SonarrReleaseDownloadBody, SonarrSerdeable}, + Serdeable, + }, + network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent}, + }; + + #[tokio::test] + async fn test_download_release_command() { + let expected_release_download_body = SonarrReleaseDownloadBody { + guid: "guid".to_owned(), + indexer_id: 1, + series_id: Some(1), + ..SonarrReleaseDownloadBody::default() + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::DownloadRelease(expected_release_download_body).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let download_release_command = SonarrDownloadCommand::Series { + guid: "guid".to_owned(), + indexer_id: 1, + series_id: 1, + }; + + let result = + SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + } +} diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 68b2b23..bc1fdcd 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -4,6 +4,7 @@ use add_command_handler::{SonarrAddCommand, SonarrAddCommandHandler}; use anyhow::Result; use clap::Subcommand; use delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler}; +use download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler}; use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler}; use list_command_handler::{SonarrListCommand, SonarrListCommandHandler}; use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler}; @@ -19,6 +20,7 @@ use super::{CliCommandHandler, Command}; mod add_command_handler; mod delete_command_handler; +mod download_command_handler; mod get_command_handler; mod list_command_handler; mod refresh_command_handler; @@ -44,6 +46,11 @@ pub enum SonarrCommand { about = "Commands to fetch details of the resources in your Sonarr instance" )] Get(SonarrGetCommand), + #[command( + subcommand, + about = "Commands to download releases in your Sonarr instance" + )] + Download(SonarrDownloadCommand), #[command( subcommand, about = "Commands to list attributes from your Sonarr instance" @@ -176,6 +183,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .handle() .await? } + SonarrCommand::Download(download_command) => { + SonarrDownloadCommandHandler::with(self.app, download_command, self.network) + .handle() + .await? + } SonarrCommand::Get(get_command) => { SonarrGetCommandHandler::with(self.app, get_command, self.network) .handle() diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index a5b580e..9a98496 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -311,14 +311,16 @@ mod tests { cli::{ sonarr::{ add_command_handler::SonarrAddCommand, delete_command_handler::SonarrDeleteCommand, - get_command_handler::SonarrGetCommand, list_command_handler::SonarrListCommand, - refresh_command_handler::SonarrRefreshCommand, SonarrCliHandler, SonarrCommand, + download_command_handler::SonarrDownloadCommand, get_command_handler::SonarrGetCommand, + list_command_handler::SonarrListCommand, refresh_command_handler::SonarrRefreshCommand, + SonarrCliHandler, SonarrCommand, }, CliCommandHandler, }, models::{ sonarr_models::{ - BlocklistItem, BlocklistResponse, Series, SonarrSerdeable, SonarrTaskName, + BlocklistItem, BlocklistResponse, Series, SonarrReleaseDownloadBody, SonarrSerdeable, + SonarrTaskName, }, Serdeable, }, @@ -500,6 +502,42 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn test_sonarr_cli_handler_delegates_download_commands_to_the_download_command_handler() { + let expected_params = SonarrReleaseDownloadBody { + guid: "1234".to_owned(), + indexer_id: 1, + series_id: Some(1), + ..SonarrReleaseDownloadBody::default() + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::DownloadRelease(expected_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let download_series_release_command = + SonarrCommand::Download(SonarrDownloadCommand::Series { + guid: "1234".to_owned(), + indexer_id: 1, + series_id: 1, + }); + + let result = + SonarrCliHandler::with(&app_arc, download_series_release_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + #[tokio::test] async fn test_sonarr_cli_handler_delegates_get_commands_to_the_get_command_handler() { let mut mock_network = MockNetworkTrait::new();