From 44d147ce375dbc44183ce2aa4ddeb5878356da51 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 20 Aug 2026 21:52:18 -0600 Subject: [PATCH] feat(readarr): fetch Readarr queued events and expose them on the CLI An empty array is a valid response when nothing is queued, so the empty case is covered by its own test. --- src/cli/readarr/list_command_handler.rs | 9 ++ src/cli/readarr/list_command_handler_tests.rs | 25 ++++- src/network/readarr_network/mod.rs | 7 +- .../readarr_network/readarr_network_tests.rs | 1 + src/network/readarr_network/system/mod.rs | 23 ++++- .../system/readarr_system_network_tests.rs | 92 ++++++++++++++++++- 6 files changed, 153 insertions(+), 4 deletions(-) diff --git a/src/cli/readarr/list_command_handler.rs b/src/cli/readarr/list_command_handler.rs index 1eac67c9..a975007a 100644 --- a/src/cli/readarr/list_command_handler.rs +++ b/src/cli/readarr/list_command_handler.rs @@ -29,6 +29,8 @@ pub enum ReadarrListCommand { )] output_in_log_format: bool, }, + #[command(about = "List all queued events")] + QueuedEvents, #[command(about = "List all Readarr tasks")] Tasks, #[command(about = "List all Readarr updates")] @@ -86,6 +88,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrListCommand> for ReadarrListComman serde_json::to_string_pretty(&logs)? } } + ReadarrListCommand::QueuedEvents => { + let resp = self + .network + .handle_network_event(ReadarrEvent::GetQueuedEvents.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } ReadarrListCommand::Tasks => { let resp = self .network diff --git a/src/cli/readarr/list_command_handler_tests.rs b/src/cli/readarr/list_command_handler_tests.rs index b0e7494b..dd4951a7 100644 --- a/src/cli/readarr/list_command_handler_tests.rs +++ b/src/cli/readarr/list_command_handler_tests.rs @@ -25,7 +25,7 @@ mod tests { #[rstest] fn test_list_commands_have_no_arg_requirements( - #[values("disk-space", "tasks", "updates")] subcommand: &str, + #[values("disk-space", "queued-events", "tasks", "updates")] subcommand: &str, ) { let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", subcommand]); @@ -161,6 +161,29 @@ mod tests { assert_str_eq!(result.unwrap(), "[\n \"readarr log line\"\n]"); } + #[tokio::test] + async fn test_handle_list_queued_events_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(ReadarrEvent::GetQueuedEvents.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Readarr(ReadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_queued_events_command = ReadarrListCommand::QueuedEvents; + + let result = + ReadarrListCommandHandler::with(&app_arc, list_queued_events_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_list_tasks_command() { let mut mock_network = MockNetworkTrait::new(); diff --git a/src/network/readarr_network/mod.rs b/src/network/readarr_network/mod.rs index 3c0b233a..1008cc8c 100644 --- a/src/network/readarr_network/mod.rs +++ b/src/network/readarr_network/mod.rs @@ -16,6 +16,7 @@ pub enum ReadarrEvent { GetDiskSpace, GetHostConfig, GetLogs(u64), + GetQueuedEvents, GetSecurityConfig, GetStatus, GetTasks, @@ -27,7 +28,7 @@ pub enum ReadarrEvent { impl NetworkResource for ReadarrEvent { fn resource(&self) -> &'static str { match &self { - ReadarrEvent::StartTask(_) => "/command", + ReadarrEvent::GetQueuedEvents | ReadarrEvent::StartTask(_) => "/command", ReadarrEvent::GetHostConfig | ReadarrEvent::GetSecurityConfig => "/config/host", ReadarrEvent::GetDiskSpace => "/diskspace", ReadarrEvent::HealthCheck => "/health", @@ -63,6 +64,10 @@ impl Network<'_, '_> { .get_readarr_logs(events) .await .map(ReadarrSerdeable::from), + ReadarrEvent::GetQueuedEvents => self + .get_queued_readarr_events() + .await + .map(ReadarrSerdeable::from), ReadarrEvent::GetSecurityConfig => self .get_readarr_security_config() .await diff --git a/src/network/readarr_network/readarr_network_tests.rs b/src/network/readarr_network/readarr_network_tests.rs index d845e79f..46da9ccd 100644 --- a/src/network/readarr_network/readarr_network_tests.rs +++ b/src/network/readarr_network/readarr_network_tests.rs @@ -6,6 +6,7 @@ mod tests { use rstest::rstest; #[rstest] + #[case(ReadarrEvent::GetQueuedEvents, "/command")] #[case(ReadarrEvent::StartTask(Default::default()), "/command")] #[case(ReadarrEvent::GetHostConfig, "/config/host")] #[case(ReadarrEvent::GetSecurityConfig, "/config/host")] diff --git a/src/network/readarr_network/system/mod.rs b/src/network/readarr_network/system/mod.rs index a20bdac8..694e2df4 100644 --- a/src/network/readarr_network/system/mod.rs +++ b/src/network/readarr_network/system/mod.rs @@ -1,6 +1,6 @@ use crate::models::readarr_models::{ReadarrTask, ReadarrTaskName}; use crate::models::servarr_models::{ - CommandBody, DiskSpace, HostConfig, LogResponse, SecurityConfig, SystemStatus, Update, + CommandBody, DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, SystemStatus, Update, }; use crate::models::{HorizontallyScrollableText, Scrollable, ScrollableText}; use crate::network::readarr_network::ReadarrEvent; @@ -15,6 +15,27 @@ use serde_json::Value; mod readarr_system_network_tests; impl Network<'_, '_> { + pub(in crate::network::readarr_network) async fn get_queued_readarr_events( + &mut self, + ) -> Result> { + info!("Fetching Readarr queued events"); + let event = ReadarrEvent::GetQueuedEvents; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |queued_events_vec, mut app| { + app + .data + .readarr_data + .queued_events + .set_items(queued_events_vec); + }) + .await + } + pub(in crate::network::readarr_network) async fn get_readarr_diskspace( &mut self, ) -> Result> { diff --git a/src/network/readarr_network/system/readarr_system_network_tests.rs b/src/network/readarr_network/system/readarr_system_network_tests.rs index 871c9b92..a95170dc 100644 --- a/src/network/readarr_network/system/readarr_system_network_tests.rs +++ b/src/network/readarr_network/system/readarr_system_network_tests.rs @@ -4,7 +4,7 @@ mod tests { use crate::models::readarr_models::{ReadarrSerdeable, ReadarrTask, ReadarrTaskName}; use crate::models::servarr_models::{ AuthenticationMethod, AuthenticationRequired, CertificateValidation, DiskSpace, HostConfig, - LogResponse, SecurityConfig, SystemStatus, Update, + LogResponse, QueueEvent, SecurityConfig, SystemStatus, Update, }; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::readarr_network::ReadarrEvent; @@ -13,6 +13,96 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use serde_json::json; + #[tokio::test] + async fn test_handle_get_queued_readarr_events_event() { + let queued_events_json = json!([{ + "name": "CheckHealth", + "commandName": "Check Health", + "status": "completed", + "queued": "2023-05-20T21:29:16Z", + "started": "2023-05-20T21:29:16Z", + "ended": "2023-05-20T21:29:16Z", + "duration": "00:00:00.3524068", + "trigger": "manual", + }]); + let response: Vec = serde_json::from_value(queued_events_json.clone()).unwrap(); + let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); + let expected_event = QueueEvent { + name: "CheckHealth".to_owned(), + command_name: "Check Health".to_owned(), + status: "completed".to_owned(), + queued: timestamp, + started: Some(timestamp), + ended: Some(timestamp), + duration: Some("00:00:00.3524068".to_owned()), + trigger: "manual".to_owned(), + }; + let (mock, app, _server) = MockServarrApi::get() + .returns(queued_events_json) + .build_for(ReadarrEvent::GetQueuedEvents) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::GetQueuedEvents) + .await; + + mock.assert_async().await; + + let ReadarrSerdeable::QueueEvents(events) = result.unwrap() else { + panic!("Expected QueueEvents") + }; + + assert_eq!(events, response); + assert_eq!( + app.lock().await.data.readarr_data.queued_events.items, + vec![expected_event] + ); + } + + #[tokio::test] + async fn test_handle_get_queued_readarr_events_event_empty_response() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!([])) + .build_for(ReadarrEvent::GetQueuedEvents) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::GetQueuedEvents) + .await; + + mock.assert_async().await; + + let ReadarrSerdeable::QueueEvents(events) = result.unwrap() else { + panic!("Expected QueueEvents") + }; + + assert_is_empty!(events); + assert_is_empty!(app.lock().await.data.readarr_data.queued_events); + } + + #[tokio::test] + async fn test_handle_get_queued_readarr_events_event_failure() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!({})) + .status(500) + .build_for(ReadarrEvent::GetQueuedEvents) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::GetQueuedEvents) + .await; + + mock.assert_async().await; + assert_err!(result); + assert_is_empty!(app.lock().await.data.readarr_data.queued_events); + } + #[tokio::test] async fn test_handle_get_readarr_diskspace_event() { let (mock, app, _server) = MockServarrApi::get()