From 4a7cf922e82016d410daccfdd44172b57c6c884a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 20 Aug 2026 22:02:34 -0600 Subject: [PATCH] feat(readarr): fetch Readarr quality profiles and expose them on the CLI --- src/cli/readarr/list_command_handler.rs | 9 ++ src/cli/readarr/list_command_handler_tests.rs | 26 +++++- src/network/readarr_network/mod.rs | 25 +++++ .../readarr_network/readarr_network_tests.rs | 91 +++++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/src/cli/readarr/list_command_handler.rs b/src/cli/readarr/list_command_handler.rs index a975007a..fb7c2383 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 Readarr quality profiles")] + QualityProfiles, #[command(about = "List all queued events")] QueuedEvents, #[command(about = "List all Readarr tasks")] @@ -88,6 +90,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrListCommand> for ReadarrListComman serde_json::to_string_pretty(&logs)? } } + ReadarrListCommand::QualityProfiles => { + let resp = self + .network + .handle_network_event(ReadarrEvent::GetQualityProfiles.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } ReadarrListCommand::QueuedEvents => { 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 dd4951a7..bd2bb76b 100644 --- a/src/cli/readarr/list_command_handler_tests.rs +++ b/src/cli/readarr/list_command_handler_tests.rs @@ -25,7 +25,8 @@ mod tests { #[rstest] fn test_list_commands_have_no_arg_requirements( - #[values("disk-space", "queued-events", "tasks", "updates")] subcommand: &str, + #[values("disk-space", "quality-profiles", "queued-events", "tasks", "updates")] + subcommand: &str, ) { let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", subcommand]); @@ -161,6 +162,29 @@ mod tests { assert_str_eq!(result.unwrap(), "[\n \"readarr log line\"\n]"); } + #[tokio::test] + async fn test_handle_list_quality_profiles_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(ReadarrEvent::GetQualityProfiles.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Readarr(ReadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_quality_profiles_command = ReadarrListCommand::QualityProfiles; + + let result = + ReadarrListCommandHandler::with(&app_arc, list_quality_profiles_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_list_queued_events_command() { let mut mock_network = MockNetworkTrait::new(); diff --git a/src/network/readarr_network/mod.rs b/src/network/readarr_network/mod.rs index 1008cc8c..1b1b0cb8 100644 --- a/src/network/readarr_network/mod.rs +++ b/src/network/readarr_network/mod.rs @@ -3,6 +3,7 @@ use log::info; use super::{NetworkEvent, NetworkResource}; use crate::models::readarr_models::{ReadarrSerdeable, ReadarrTaskName}; +use crate::models::servarr_models::QualityProfile; use crate::network::{Network, RequestMethod}; mod system; @@ -16,6 +17,7 @@ pub enum ReadarrEvent { GetDiskSpace, GetHostConfig, GetLogs(u64), + GetQualityProfiles, GetQueuedEvents, GetSecurityConfig, GetStatus, @@ -33,6 +35,7 @@ impl NetworkResource for ReadarrEvent { ReadarrEvent::GetDiskSpace => "/diskspace", ReadarrEvent::HealthCheck => "/health", ReadarrEvent::GetLogs(_) => "/log", + ReadarrEvent::GetQualityProfiles => "/qualityprofile", ReadarrEvent::GetStatus => "/system/status", ReadarrEvent::GetTasks => "/system/task", ReadarrEvent::GetUpdates => "/update", @@ -64,6 +67,10 @@ impl Network<'_, '_> { .get_readarr_logs(events) .await .map(ReadarrSerdeable::from), + ReadarrEvent::GetQualityProfiles => self + .get_readarr_quality_profiles() + .await + .map(ReadarrSerdeable::from), ReadarrEvent::GetQueuedEvents => self .get_queued_readarr_events() .await @@ -100,4 +107,22 @@ impl Network<'_, '_> { .handle_request::<(), ()>(request_props, |_, _| ()) .await } + + async fn get_readarr_quality_profiles(&mut self) -> Result> { + info!("Fetching Readarr quality profiles"); + let event = ReadarrEvent::GetQualityProfiles; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |quality_profiles, mut app| { + app.data.readarr_data.quality_profile_map = quality_profiles + .into_iter() + .map(|profile| (profile.id, profile.name)) + .collect(); + }) + .await + } } diff --git a/src/network/readarr_network/readarr_network_tests.rs b/src/network/readarr_network/readarr_network_tests.rs index 46da9ccd..732b375c 100644 --- a/src/network/readarr_network/readarr_network_tests.rs +++ b/src/network/readarr_network/readarr_network_tests.rs @@ -1,9 +1,12 @@ #[cfg(test)] mod tests { + use crate::models::readarr_models::ReadarrSerdeable; + use crate::models::servarr_models::QualityProfile; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::{NetworkEvent, NetworkResource, readarr_network::ReadarrEvent}; use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; + use serde_json::json; #[rstest] #[case(ReadarrEvent::GetQueuedEvents, "/command")] @@ -13,6 +16,7 @@ mod tests { #[case(ReadarrEvent::GetDiskSpace, "/diskspace")] #[case(ReadarrEvent::HealthCheck, "/health")] #[case(ReadarrEvent::GetLogs(500), "/log")] + #[case(ReadarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(ReadarrEvent::GetStatus, "/system/status")] #[case(ReadarrEvent::GetTasks, "/system/task")] #[case(ReadarrEvent::GetUpdates, "/update")] @@ -59,4 +63,91 @@ mod tests { mock.assert_async().await; assert_err!(result); } + + #[tokio::test] + async fn test_handle_get_quality_profiles_event() { + let quality_profiles_json = json!([ + { + "id": 1, + "name": "eBook" + }, + { + "id": 2, + "name": "Spoken" + } + ]); + let response: Vec = + serde_json::from_value(quality_profiles_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(quality_profiles_json) + .build_for(ReadarrEvent::GetQualityProfiles) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::GetQualityProfiles) + .await; + + mock.assert_async().await; + + let ReadarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else { + panic!("Expected QualityProfiles"); + }; + + assert_eq!(quality_profiles, response); + + let app = app.lock().await; + + assert_some_eq_x!( + app.data.readarr_data.quality_profile_map.get_by_left(&1), + &"eBook".to_owned() + ); + assert_some_eq_x!( + app.data.readarr_data.quality_profile_map.get_by_left(&2), + &"Spoken".to_owned() + ); + } + + #[tokio::test] + async fn test_handle_get_quality_profiles_event_empty_response() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!([])) + .build_for(ReadarrEvent::GetQualityProfiles) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::GetQualityProfiles) + .await; + + mock.assert_async().await; + + let ReadarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else { + panic!("Expected QualityProfiles"); + }; + + assert_is_empty!(quality_profiles); + assert_is_empty!(app.lock().await.data.readarr_data.quality_profile_map); + } + + #[tokio::test] + async fn test_handle_get_quality_profiles_event_failure() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!({})) + .status(500) + .build_for(ReadarrEvent::GetQualityProfiles) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::GetQualityProfiles) + .await; + + mock.assert_async().await; + assert_err!(result); + assert_is_empty!(app.lock().await.data.readarr_data.quality_profile_map); + } }