diff --git a/src/cli/readarr/mod.rs b/src/cli/readarr/mod.rs index b6b325b5..f0295f61 100644 --- a/src/cli/readarr/mod.rs +++ b/src/cli/readarr/mod.rs @@ -16,6 +16,7 @@ use trigger_automatic_search_command_handler::{ use super::{CliCommandHandler, Command}; use crate::models::readarr_models::ReadarrTaskName; +use crate::models::servarr_models::ReleaseDownloadBody; use crate::network::readarr_network::ReadarrEvent; use crate::{app::App, network::NetworkTrait}; @@ -68,6 +69,17 @@ pub enum ReadarrCommand { about = "Commands to trigger automatic searches for releases of different resources in your Readarr instance" )] TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand), + #[command(about = "Manually download the given release")] + DownloadRelease { + #[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, + }, #[command(about = "Mark the Readarr history item with the given ID as 'failed'")] MarkHistoryItemAsFailed { #[arg( @@ -184,6 +196,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, .handle() .await? } + ReadarrCommand::DownloadRelease { guid, indexer_id } => { + let params = ReleaseDownloadBody { guid, indexer_id }; + let resp = self + .network + .handle_network_event(ReadarrEvent::DownloadRelease(params).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } ReadarrCommand::MarkHistoryItemAsFailed { history_item_id } => { let _ = self .network diff --git a/src/cli/readarr/readarr_command_tests.rs b/src/cli/readarr/readarr_command_tests.rs index afcdf9ed..a4a19013 100644 --- a/src/cli/readarr/readarr_command_tests.rs +++ b/src/cli/readarr/readarr_command_tests.rs @@ -26,6 +26,55 @@ mod tests { ); } + #[test] + fn test_download_release_requires_guid() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "readarr", + "download-release", + "--indexer-id", + "6", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_download_release_requires_indexer_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "readarr", + "download-release", + "--guid", + "test-release-guid", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_download_release_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "readarr", + "download-release", + "--guid", + "test-release-guid", + "--indexer-id", + "6", + ]); + + assert_ok!(&result); + } + #[test] fn test_mark_history_item_as_failed_requirements_satisfied() { let result = Cli::command().try_get_matches_from([ @@ -161,6 +210,7 @@ mod tests { use tokio::sync::Mutex; use crate::models::readarr_models::{ReadarrSerdeable, ReadarrTaskName}; + use crate::models::servarr_models::ReleaseDownloadBody; use crate::{ app::App, cli::{ @@ -202,6 +252,37 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_download_release_command() { + let expected_release_download_body = ReleaseDownloadBody { + guid: "test-release-guid".to_owned(), + indexer_id: 6, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + ReadarrEvent::DownloadRelease(expected_release_download_body).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Readarr(ReadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let download_release_command = ReadarrCommand::DownloadRelease { + guid: "test-release-guid".to_owned(), + indexer_id: 6, + }; + + let result = ReadarrCliHandler::with(&app_arc, download_release_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_search_new_author_command() { let expected_query = "test author".to_owned(); diff --git a/src/network/readarr_network/library/mod.rs b/src/network/readarr_network/library/mod.rs index c2fc5060..bf8c3725 100644 --- a/src/network/readarr_network/library/mod.rs +++ b/src/network/readarr_network/library/mod.rs @@ -1,2 +1,56 @@ +use crate::models::servarr_data::Notification; +use crate::models::servarr_models::ReleaseDownloadBody; +use crate::network::readarr_network::ReadarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; +use serde_json::Value; + mod authors; mod books; + +#[cfg(test)] +#[path = "readarr_library_network_tests.rs"] +mod readarr_library_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::readarr_network) async fn download_readarr_release( + &mut self, + release_download_body: ReleaseDownloadBody, + ) -> Result { + let event = ReadarrEvent::DownloadRelease(ReleaseDownloadBody::default()); + info!("Downloading Readarr release with params: {release_download_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + Some(release_download_body), + None, + None, + ) + .await; + + let result = self + .handle_request::(request_props, |_, mut app| { + app.notification = Some(Notification::new( + "Download Result".to_owned(), + "Download request sent successfully".to_owned(), + true, + )); + }) + .await; + + if result.is_err() { + let mut app = self.app.lock().await; + std::mem::take(&mut app.error.text); + app.notification = Some(Notification::new( + "Download Failed".to_owned(), + "Download request failed. Check the logs for more details.".to_owned(), + false, + )); + } + + result + } +} diff --git a/src/network/readarr_network/library/readarr_library_network_tests.rs b/src/network/readarr_network/library/readarr_library_network_tests.rs new file mode 100644 index 00000000..7268e40c --- /dev/null +++ b/src/network/readarr_network/library/readarr_library_network_tests.rs @@ -0,0 +1,82 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::Notification; + use crate::models::servarr_models::ReleaseDownloadBody; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use crate::network::readarr_network::ReadarrEvent; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[tokio::test] + async fn test_handle_download_readarr_release_event_uses_provided_params() { + let params = ReleaseDownloadBody { + guid: "test-release-guid".to_owned(), + indexer_id: 6, + }; + + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "guid": "test-release-guid", + "indexerId": 6, + })) + .returns(json!({ "id": 3 })) + .build_for(ReadarrEvent::DownloadRelease(params.clone())) + .await; + + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::DownloadRelease(params)) + .await; + + mock.assert_async().await; + assert_ok!(result); + assert_eq!( + app.lock().await.notification, + Some(Notification::new( + "Download Result".to_owned(), + "Download request sent successfully".to_owned(), + true, + )) + ); + } + + #[tokio::test] + async fn test_handle_download_readarr_release_event_sets_failure_notification_on_error() { + let params = ReleaseDownloadBody { + guid: "test-release-guid".to_owned(), + indexer_id: 6, + }; + + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "guid": "test-release-guid", + "indexerId": 6, + })) + .returns(json!({ "id": 3 })) + .status(500) + .build_for(ReadarrEvent::DownloadRelease(params.clone())) + .await; + + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::DownloadRelease(params)) + .await; + + mock.assert_async().await; + assert_err!(result); + let app = app.lock().await; + assert_is_empty!(app.error.text); + assert_some_eq_x!( + &app.notification, + &Notification::new( + "Download Failed".to_owned(), + "Download request failed. Check the logs for more details.".to_owned(), + false, + ) + ); + } +} diff --git a/src/network/readarr_network/mod.rs b/src/network/readarr_network/mod.rs index b2c1ce7d..8a1eadb7 100644 --- a/src/network/readarr_network/mod.rs +++ b/src/network/readarr_network/mod.rs @@ -7,7 +7,7 @@ use crate::models::readarr_models::{ AddAuthorBody, AddReadarrRootFolderBody, DeleteParams, EditAuthorParams, ReadarrSerdeable, ReadarrTaskName, }; -use crate::models::servarr_models::{MetadataProfile, QualityProfile, Tag}; +use crate::models::servarr_models::{MetadataProfile, QualityProfile, ReleaseDownloadBody, Tag}; use crate::network::{Network, RequestMethod}; mod downloads; @@ -35,6 +35,7 @@ pub enum ReadarrEvent { DeleteDownload(i64), DeleteRootFolder(i64), DeleteTag(i64), + DownloadRelease(ReleaseDownloadBody), EditAuthor(EditAuthorParams), GetAuthorDetails(i64), GetAuthorHistory(i64), @@ -106,7 +107,9 @@ impl NetworkResource for ReadarrEvent { ReadarrEvent::GetMetadataProfiles => "/metadataprofile", ReadarrEvent::GetQualityProfiles => "/qualityprofile", ReadarrEvent::DeleteDownload(_) | ReadarrEvent::GetDownloads(_) => "/queue", - ReadarrEvent::GetAuthorReleases(_) | ReadarrEvent::GetBookReleases(_) => "/release", + ReadarrEvent::DownloadRelease(_) + | ReadarrEvent::GetAuthorReleases(_) + | ReadarrEvent::GetBookReleases(_) => "/release", ReadarrEvent::AddRootFolder(_) | ReadarrEvent::DeleteRootFolder(_) | ReadarrEvent::GetRootFolders => "/rootfolder", @@ -160,6 +163,10 @@ impl Network<'_, '_> { .delete_readarr_tag(tag_id) .await .map(ReadarrSerdeable::from), + ReadarrEvent::DownloadRelease(release_download_body) => self + .download_readarr_release(release_download_body) + .await + .map(ReadarrSerdeable::from), ReadarrEvent::EditAuthor(edit_author_params) => self .edit_author(edit_author_params) .await diff --git a/src/network/readarr_network/readarr_network_tests.rs b/src/network/readarr_network/readarr_network_tests.rs index d3637a9c..d4fa1326 100644 --- a/src/network/readarr_network/readarr_network_tests.rs +++ b/src/network/readarr_network/readarr_network_tests.rs @@ -4,7 +4,7 @@ mod tests { use crate::models::readarr_models::{ AddAuthorBody, AddReadarrRootFolderBody, DeleteParams, EditAuthorParams, ReadarrSerdeable, }; - use crate::models::servarr_models::{MetadataProfile, QualityProfile, Tag}; + use crate::models::servarr_models::{MetadataProfile, QualityProfile, ReleaseDownloadBody, Tag}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::{NetworkEvent, NetworkResource, readarr_network::ReadarrEvent}; use bimap::BiMap; @@ -49,6 +49,10 @@ mod tests { #[case(ReadarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(ReadarrEvent::DeleteDownload(1), "/queue")] #[case(ReadarrEvent::GetDownloads(500), "/queue")] + #[case( + ReadarrEvent::DownloadRelease(ReleaseDownloadBody::default()), + "/release" + )] #[case(ReadarrEvent::GetAuthorReleases(1), "/release")] #[case(ReadarrEvent::GetBookReleases(1), "/release")] #[case(