From 1d8ccd7ff6684085e3819fe39ce2945b317f7126 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 20 Aug 2026 22:06:23 -0600 Subject: [PATCH] feat(readarr): fetch Readarr tags and expose them on the CLI --- src/cli/readarr/list_command_handler.rs | 9 +++ src/cli/readarr/list_command_handler_tests.rs | 23 ++++++ src/network/readarr_network/mod.rs | 23 +++++- .../readarr_network/readarr_network_tests.rs | 72 ++++++++++++++++++- 4 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/cli/readarr/list_command_handler.rs b/src/cli/readarr/list_command_handler.rs index 7b2f570f..82c0777f 100644 --- a/src/cli/readarr/list_command_handler.rs +++ b/src/cli/readarr/list_command_handler.rs @@ -35,6 +35,8 @@ pub enum ReadarrListCommand { QualityProfiles, #[command(about = "List all queued events")] QueuedEvents, + #[command(about = "List all Readarr tags")] + Tags, #[command(about = "List all Readarr tasks")] Tasks, #[command(about = "List all Readarr updates")] @@ -113,6 +115,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrListCommand> for ReadarrListComman .await?; serde_json::to_string_pretty(&resp)? } + ReadarrListCommand::Tags => { + let resp = self + .network + .handle_network_event(ReadarrEvent::GetTags.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 2e32c43d..4b0e45c9 100644 --- a/src/cli/readarr/list_command_handler_tests.rs +++ b/src/cli/readarr/list_command_handler_tests.rs @@ -30,6 +30,7 @@ mod tests { "metadata-profiles", "quality-profiles", "queued-events", + "tags", "tasks", "updates" )] @@ -241,6 +242,28 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_handle_list_tags_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(ReadarrEvent::GetTags.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Readarr(ReadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_tags_command = ReadarrListCommand::Tags; + + let result = ReadarrListCommandHandler::with(&app_arc, list_tags_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 5ebf41c2..91b15cd0 100644 --- a/src/network/readarr_network/mod.rs +++ b/src/network/readarr_network/mod.rs @@ -3,7 +3,7 @@ use log::info; use super::{NetworkEvent, NetworkResource}; use crate::models::readarr_models::{ReadarrSerdeable, ReadarrTaskName}; -use crate::models::servarr_models::{MetadataProfile, QualityProfile}; +use crate::models::servarr_models::{MetadataProfile, QualityProfile, Tag}; use crate::network::{Network, RequestMethod}; mod system; @@ -22,6 +22,7 @@ pub enum ReadarrEvent { GetQueuedEvents, GetSecurityConfig, GetStatus, + GetTags, GetTasks, GetUpdates, HealthCheck, @@ -40,6 +41,7 @@ impl NetworkResource for ReadarrEvent { ReadarrEvent::GetQualityProfiles => "/qualityprofile", ReadarrEvent::GetStatus => "/system/status", ReadarrEvent::GetTasks => "/system/task", + ReadarrEvent::GetTags => "/tag", ReadarrEvent::GetUpdates => "/update", } } @@ -86,6 +88,7 @@ impl Network<'_, '_> { .await .map(ReadarrSerdeable::from), ReadarrEvent::GetStatus => self.get_readarr_status().await.map(ReadarrSerdeable::from), + ReadarrEvent::GetTags => self.get_readarr_tags().await.map(ReadarrSerdeable::from), ReadarrEvent::GetTasks => self.get_readarr_tasks().await.map(ReadarrSerdeable::from), ReadarrEvent::GetUpdates => self.get_readarr_updates().await.map(ReadarrSerdeable::from), ReadarrEvent::HealthCheck => self @@ -149,4 +152,22 @@ impl Network<'_, '_> { }) .await } + + async fn get_readarr_tags(&mut self) -> Result> { + info!("Fetching Readarr tags"); + let event = ReadarrEvent::GetTags; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |tags_vec, mut app| { + app.data.readarr_data.tags_map = tags_vec + .into_iter() + .map(|tag| (tag.id, tag.label)) + .collect(); + }) + .await + } } diff --git a/src/network/readarr_network/readarr_network_tests.rs b/src/network/readarr_network/readarr_network_tests.rs index 6bb3b8a9..91d8d093 100644 --- a/src/network/readarr_network/readarr_network_tests.rs +++ b/src/network/readarr_network/readarr_network_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use crate::models::readarr_models::ReadarrSerdeable; - use crate::models::servarr_models::{MetadataProfile, QualityProfile}; + use crate::models::servarr_models::{MetadataProfile, QualityProfile, Tag}; 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}; @@ -20,6 +20,7 @@ mod tests { #[case(ReadarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(ReadarrEvent::GetStatus, "/system/status")] #[case(ReadarrEvent::GetTasks, "/system/task")] + #[case(ReadarrEvent::GetTags, "/tag")] #[case(ReadarrEvent::GetUpdates, "/update")] fn test_resource(#[case] event: ReadarrEvent, #[case] expected_uri: &str) { assert_str_eq!(event.resource(), expected_uri); @@ -238,4 +239,73 @@ mod tests { assert_err!(result); assert_is_empty!(app.lock().await.data.readarr_data.quality_profile_map); } + + #[tokio::test] + async fn test_handle_get_tags_event() { + let tags_json = json!([ + { + "id": 1, + "label": "huntarr-missing" + } + ]); + let response: Vec = serde_json::from_value(tags_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(tags_json) + .build_for(ReadarrEvent::GetTags) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network.handle_readarr_event(ReadarrEvent::GetTags).await; + + mock.assert_async().await; + + let ReadarrSerdeable::Tags(tags) = result.unwrap() else { + panic!("Expected Tags"); + }; + + assert_eq!(tags, response); + assert_some_eq_x!( + app.lock().await.data.readarr_data.tags_map.get_by_left(&1), + &"huntarr-missing".to_owned() + ); + } + + #[tokio::test] + async fn test_handle_get_tags_event_empty_response() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!([])) + .build_for(ReadarrEvent::GetTags) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network.handle_readarr_event(ReadarrEvent::GetTags).await; + + mock.assert_async().await; + + let ReadarrSerdeable::Tags(tags) = result.unwrap() else { + panic!("Expected Tags"); + }; + + assert_is_empty!(tags); + assert_is_empty!(app.lock().await.data.readarr_data.tags_map); + } + + #[tokio::test] + async fn test_handle_get_tags_event_failure() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!({})) + .status(500) + .build_for(ReadarrEvent::GetTags) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network.handle_readarr_event(ReadarrEvent::GetTags).await; + + mock.assert_async().await; + assert_err!(result); + assert_is_empty!(app.lock().await.data.readarr_data.tags_map); + } }