diff --git a/src/cli/readarr/mod.rs b/src/cli/readarr/mod.rs index 737ebb4f..5363005e 100644 --- a/src/cli/readarr/mod.rs +++ b/src/cli/readarr/mod.rs @@ -97,6 +97,15 @@ pub enum ReadarrCommand { )] author_id: i64, }, + #[command(about = "Toggle monitoring for the specified book corresponding to the given book ID")] + ToggleBookMonitoring { + #[arg( + long, + help = "The Readarr ID of the book to toggle monitoring on", + required = true + )] + book_id: i64, + }, } impl From for Command { @@ -186,6 +195,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, .await?; serde_json::to_string_pretty(&resp)? } + ReadarrCommand::ToggleBookMonitoring { book_id } => { + let resp = self + .network + .handle_network_event(ReadarrEvent::ToggleBookMonitoring(book_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } }; Ok(result) diff --git a/src/cli/readarr/readarr_command_tests.rs b/src/cli/readarr/readarr_command_tests.rs index e702c4db..9bcc7e4e 100644 --- a/src/cli/readarr/readarr_command_tests.rs +++ b/src/cli/readarr/readarr_command_tests.rs @@ -101,6 +101,31 @@ mod tests { assert_ok!(&result); } + + #[test] + fn test_toggle_book_monitoring_requires_book_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "readarr", "toggle-book-monitoring"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_toggle_book_monitoring_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "readarr", + "toggle-book-monitoring", + "--book-id", + "1", + ]); + + assert_ok!(&result); + } } mod handler { @@ -203,5 +228,30 @@ mod tests { assert_ok!(&result); } + + #[tokio::test] + async fn test_toggle_book_monitoring_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + ReadarrEvent::ToggleBookMonitoring(1).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Readarr(ReadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let toggle_book_monitoring_command = ReadarrCommand::ToggleBookMonitoring { book_id: 1 }; + + let result = + ReadarrCliHandler::with(&app_arc, toggle_book_monitoring_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } } } diff --git a/src/network/readarr_network/library/books/mod.rs b/src/network/readarr_network/library/books/mod.rs index cf696042..2f4e9d56 100644 --- a/src/network/readarr_network/library/books/mod.rs +++ b/src/network/readarr_network/library/books/mod.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use log::info; -use serde_json::Value; +use log::{debug, info, warn}; +use serde_json::{Value, json}; use crate::models::Route; use crate::models::readarr_models::{ @@ -163,6 +163,69 @@ impl Network<'_, '_> { .await } + pub(in crate::network::readarr_network) async fn toggle_book_monitoring( + &mut self, + book_id: i64, + ) -> Result<()> { + let event = ReadarrEvent::ToggleBookMonitoring(book_id); + + let detail_event = ReadarrEvent::GetBookDetails(book_id); + info!("Toggling book monitoring for book with ID: {book_id}"); + info!("Fetching book details for book with ID: {book_id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{book_id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_book_body, _| { + response = detailed_book_body.to_string() + }) + .await?; + + info!("Constructing toggle book monitoring body"); + + match serde_json::from_str::(&response) { + Ok(mut detailed_book_body) => { + let monitored = detailed_book_body + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); + + *detailed_book_body.get_mut("monitored").unwrap() = json!(!monitored); + + debug!("Toggle book monitoring body: {detailed_book_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_book_body), + Some(format!("/{book_id}")), + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + Err(_) => { + warn!("Request for detailed book body was interrupted"); + Ok(()) + } + } + } + pub(in crate::network::readarr_network) async fn trigger_automatic_book_search( &mut self, book_id: i64, diff --git a/src/network/readarr_network/library/books/readarr_books_network_tests.rs b/src/network/readarr_network/library/books/readarr_books_network_tests.rs index 7ad77453..d3b88315 100644 --- a/src/network/readarr_network/library/books/readarr_books_network_tests.rs +++ b/src/network/readarr_network/library/books/readarr_books_network_tests.rs @@ -6,12 +6,14 @@ mod tests { use crate::models::servarr_data::readarr::modals::BookDetailsModal; use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; use crate::models::stateful_table::SortOption; + use crate::network::NetworkResource; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::readarr_network::ReadarrEvent; use crate::network::readarr_network::readarr_network_test_utils::test_utils::{ BOOK_FILE_JSON, BOOK_JSON, EDITION_JSON, readarr_history_item, stale_book, stale_book_file, stale_edition, stale_readarr_history_item, }; + use mockito::Matcher; use pretty_assertions::assert_eq; use rstest::rstest; use serde_json::{Value, json}; @@ -770,6 +772,83 @@ mod tests { ); } + #[rstest] + #[case(true, false)] + #[case(false, true)] + #[tokio::test] + async fn test_handle_toggle_book_monitoring_event( + #[case] initial_monitored: bool, + #[case] expected_monitored: bool, + ) { + let mut book_json: Value = serde_json::from_str(BOOK_JSON).unwrap(); + *book_json.get_mut("monitored").unwrap() = json!(initial_monitored); + let mut expected_body = book_json.clone(); + *expected_body.get_mut("monitored").unwrap() = json!(expected_monitored); + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(book_json) + .path("/1") + .build_for(ReadarrEvent::GetBookDetails(1)) + .await; + let async_toggle_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1", + ReadarrEvent::ToggleBookMonitoring(1).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::ToggleBookMonitoring(1)) + .await; + + async_details_server.assert_async().await; + async_toggle_server.assert_async().await; + assert_ok!(result); + } + + #[tokio::test] + async fn test_handle_toggle_book_monitoring_event_failure() { + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(serde_json::from_str(BOOK_JSON).unwrap()) + .status(404) + .path("/1") + .build_for(ReadarrEvent::GetBookDetails(1)) + .await; + let async_toggle_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1", + ReadarrEvent::ToggleBookMonitoring(1).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .expect(0) + .create_async() + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::ToggleBookMonitoring(1)) + .await; + + async_details_server.assert_async().await; + async_toggle_server.assert_async().await; + assert_err!(result); + } + #[tokio::test] async fn test_handle_trigger_automatic_book_search_event() { let (mock, app, _server) = MockServarrApi::post() diff --git a/src/network/readarr_network/mod.rs b/src/network/readarr_network/mod.rs index 893fe95a..108a7e20 100644 --- a/src/network/readarr_network/mod.rs +++ b/src/network/readarr_network/mod.rs @@ -55,6 +55,7 @@ pub enum ReadarrEvent { SearchNewAuthor(String), StartTask(ReadarrTaskName), ToggleAuthorMonitoring(i64), + ToggleBookMonitoring(i64), TriggerAutomaticAuthorSearch(i64), TriggerAutomaticBookSearch(i64), UpdateAllAuthors, @@ -71,7 +72,9 @@ impl NetworkResource for ReadarrEvent { | ReadarrEvent::ListAuthors | ReadarrEvent::ToggleAuthorMonitoring(_) => "/author", ReadarrEvent::SearchNewAuthor(_) => "/author/lookup", - ReadarrEvent::GetBookDetails(_) | ReadarrEvent::GetBooks(_) => "/book", + ReadarrEvent::GetBookDetails(_) + | ReadarrEvent::GetBooks(_) + | ReadarrEvent::ToggleBookMonitoring(_) => "/book", ReadarrEvent::GetBookFiles(_) => "/bookfile", ReadarrEvent::GetQueuedEvents | ReadarrEvent::StartTask(_) @@ -211,6 +214,10 @@ impl Network<'_, '_> { .toggle_author_monitoring(author_id) .await .map(ReadarrSerdeable::from), + ReadarrEvent::ToggleBookMonitoring(book_id) => self + .toggle_book_monitoring(book_id) + .await + .map(ReadarrSerdeable::from), ReadarrEvent::TriggerAutomaticAuthorSearch(author_id) => self .trigger_automatic_author_search(author_id) .await diff --git a/src/network/readarr_network/readarr_network_test_utils.rs b/src/network/readarr_network/readarr_network_test_utils.rs index 3b4a2335..85e60f99 100644 --- a/src/network/readarr_network/readarr_network_test_utils.rs +++ b/src/network/readarr_network/readarr_network_test_utils.rs @@ -51,6 +51,8 @@ pub mod test_utils { pub const BOOK_JSON: &str = r#"{ "id": 1, "title": "Test Book", + "authorTitle": "Test Author Test Book", + "seriesTitle": "Test Series", "authorId": 1, "foreignBookId": "test-foreign-book-id", "monitored": true, @@ -64,6 +66,7 @@ pub mod test_utils { "sizeOnDisk": 12345, "percentOfBooks": 66.6 }, + "lastSearchTime": "2023-01-01T00:00:00Z", "grabbed": false }"#; diff --git a/src/network/readarr_network/readarr_network_tests.rs b/src/network/readarr_network/readarr_network_tests.rs index 64155d60..a443d127 100644 --- a/src/network/readarr_network/readarr_network_tests.rs +++ b/src/network/readarr_network/readarr_network_tests.rs @@ -24,6 +24,7 @@ mod tests { #[case(ReadarrEvent::SearchNewAuthor(String::new()), "/author/lookup")] #[case(ReadarrEvent::GetBookDetails(1), "/book")] #[case(ReadarrEvent::GetBooks(1), "/book")] + #[case(ReadarrEvent::ToggleBookMonitoring(1), "/book")] #[case(ReadarrEvent::GetBookFiles(1), "/bookfile")] #[case(ReadarrEvent::GetQueuedEvents, "/command")] #[case(ReadarrEvent::StartTask(Default::default()), "/command")]