From 5afee1998b73b3effa94ba7f3b1bc82f70cd8505 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 6 Jan 2026 09:40:16 -0700 Subject: [PATCH] feat: Support for toggling the monitoring of a given artist via the CLI and TUI --- src/app/lidarr/lidarr_context_clues.rs | 6 +- src/app/lidarr/lidarr_context_clues_tests.rs | 7 + src/cli/lidarr/lidarr_command_tests.rs | 55 ++++++ src/cli/lidarr/mod.rs | 21 +- .../library/library_handler_tests.rs | 53 +++++ src/handlers/lidarr_handlers/library/mod.rs | 26 ++- src/models/lidarr_models.rs | 40 +++- src/models/lidarr_models_tests.rs | 185 ++++++++++++++++++ .../library/lidarr_library_network_tests.rs | 86 ++++++++ src/network/lidarr_network/library/mod.rs | 88 ++++++++- .../lidarr_network/lidarr_network_tests.rs | 13 +- src/network/lidarr_network/mod.rs | 15 +- 12 files changed, 583 insertions(+), 12 deletions(-) diff --git a/src/app/lidarr/lidarr_context_clues.rs b/src/app/lidarr/lidarr_context_clues.rs index 772cb27..cf0b3c9 100644 --- a/src/app/lidarr/lidarr_context_clues.rs +++ b/src/app/lidarr/lidarr_context_clues.rs @@ -7,7 +7,11 @@ use crate::models::Route; #[path = "lidarr_context_clues_tests.rs"] mod lidarr_context_clues_tests; -pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 6] = [ +pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 7] = [ + ( + DEFAULT_KEYBINDINGS.toggle_monitoring, + DEFAULT_KEYBINDINGS.toggle_monitoring.desc, + ), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), diff --git a/src/app/lidarr/lidarr_context_clues_tests.rs b/src/app/lidarr/lidarr_context_clues_tests.rs index 4e60644..542469a 100644 --- a/src/app/lidarr/lidarr_context_clues_tests.rs +++ b/src/app/lidarr/lidarr_context_clues_tests.rs @@ -13,6 +13,13 @@ mod tests { fn test_artists_context_clues() { let mut artists_context_clues_iter = ARTISTS_CONTEXT_CLUES.iter(); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.toggle_monitoring, + DEFAULT_KEYBINDINGS.toggle_monitoring.desc + ) + ); assert_some_eq_x!( artists_context_clues_iter.next(), &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) diff --git a/src/cli/lidarr/lidarr_command_tests.rs b/src/cli/lidarr/lidarr_command_tests.rs index 0262b07..dede7cc 100644 --- a/src/cli/lidarr/lidarr_command_tests.rs +++ b/src/cli/lidarr/lidarr_command_tests.rs @@ -19,6 +19,8 @@ mod tests { mod cli { use super::*; + use clap::error::ErrorKind; + use pretty_assertions::assert_eq; #[test] fn test_list_artists_has_no_arg_requirements() { @@ -40,6 +42,31 @@ mod tests { assert_err!(&result); } + + #[test] + fn test_toggle_artist_monitoring_requires_artist_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-artist-monitoring"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_toggle_artist_monitoring_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "toggle-artist-monitoring", + "--artist-id", + "1", + ]); + + assert_ok!(&result); + } } mod handler { @@ -119,5 +146,33 @@ mod tests { assert_ok!(&result); } + + #[tokio::test] + async fn test_toggle_artist_monitoring_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::ToggleArtistMonitoring(1).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let toggle_artist_monitoring_command = LidarrCommand::ToggleArtistMonitoring { artist_id: 1 }; + + let result = LidarrCliHandler::with( + &app_arc, + toggle_artist_monitoring_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } } } diff --git a/src/cli/lidarr/mod.rs b/src/cli/lidarr/mod.rs index 1251bbb..43f2bed 100644 --- a/src/cli/lidarr/mod.rs +++ b/src/cli/lidarr/mod.rs @@ -1,11 +1,12 @@ use std::sync::Arc; use anyhow::Result; -use clap::Subcommand; +use clap::{Subcommand, arg}; use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler}; use list_command_handler::{LidarrListCommand, LidarrListCommandHandler}; use tokio::sync::Mutex; +use crate::network::lidarr_network::LidarrEvent; use crate::{app::App, network::NetworkTrait}; use super::{CliCommandHandler, Command}; @@ -29,6 +30,17 @@ pub enum LidarrCommand { about = "Commands to list attributes from your Lidarr instance" )] List(LidarrListCommand), + #[command( + about = "Toggle monitoring for the specified artist corresponding to the given artist ID" + )] + ToggleArtistMonitoring { + #[arg( + long, + help = "The Lidarr ID of the artist to toggle monitoring on", + required = true + )] + artist_id: i64, + }, } impl From for Command { @@ -68,6 +80,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, ' .handle() .await? } + LidarrCommand::ToggleArtistMonitoring { artist_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::ToggleArtistMonitoring(artist_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } }; Ok(result) diff --git a/src/handlers/lidarr_handlers/library/library_handler_tests.rs b/src/handlers/lidarr_handlers/library/library_handler_tests.rs index e2e17d6..f3bdc24 100644 --- a/src/handlers/lidarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/lidarr_handlers/library/library_handler_tests.rs @@ -6,10 +6,14 @@ mod tests { use serde_json::Number; use strum::IntoEnumIterator; + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; use crate::handlers::KeyEventHandler; use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options}; use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus}; use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS}; + use crate::network::lidarr_network::LidarrEvent; #[test] fn test_library_handler_accepts() { @@ -214,6 +218,55 @@ mod tests { assert_str_eq!(sort_option.name, "Tags"); } + #[test] + fn test_toggle_monitoring_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_routing = false; + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(app.is_routing); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::ToggleArtistMonitoring(0) + ); + } + + #[test] + fn test_toggle_monitoring_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_routing = false; + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_modal_absent!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.is_routing); + } + fn artists_vec() -> Vec { vec![ Artist { diff --git a/src/handlers/lidarr_handlers/library/mod.rs b/src/handlers/lidarr_handlers/library/mod.rs index ba82132..82c87c0 100644 --- a/src/handlers/lidarr_handlers/library/mod.rs +++ b/src/handlers/lidarr_handlers/library/mod.rs @@ -11,6 +11,7 @@ use crate::{ }, stateful_table::SortOption, }, + network::lidarr_network::LidarrEvent, }; use super::handle_change_tab_left_right_keys; @@ -31,6 +32,12 @@ pub(super) struct LibraryHandler<'a, 'b> { _context: Option, } +impl LibraryHandler<'_, '_> { + fn extract_artist_id(&self) -> i64 { + self.app.data.lidarr_data.artists.current_selection().id + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, 'b> { fn handle(&mut self) { let artists_table_handling_config = TableHandlingConfig::new(ActiveLidarrBlock::Artists.into()) @@ -114,8 +121,23 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, ' fn handle_char_key_event(&mut self) { let key = self.key; - if self.active_lidarr_block == ActiveLidarrBlock::Artists && matches_key!(refresh, key) { - self.app.should_refresh = true; + if self.active_lidarr_block == ActiveLidarrBlock::Artists { + match key { + _ if matches_key!(toggle_monitoring, key) => { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some( + LidarrEvent::ToggleArtistMonitoring(self.extract_artist_id()), + ); + + self + .app + .pop_and_push_navigation_stack(self.active_lidarr_block.into()); + } + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ => (), + } } } diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index 383bca8..bdb8706 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -5,7 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::{Number, Value}; use strum::{Display, EnumIter}; -use super::{HorizontallyScrollableText, Serdeable}; +use super::{ + HorizontallyScrollableText, Serdeable, + servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag}, +}; use crate::serde_enum_from; #[cfg(test)] @@ -29,6 +32,7 @@ pub struct Artist { #[serde(deserialize_with = "super::from_i64")] pub metadata_profile_id: i64, pub monitored: bool, + pub monitor_new_items: NewItemMonitorType, pub genres: Vec, pub tags: Vec, pub added: DateTime, @@ -94,6 +98,31 @@ impl From<(&i64, &String)> for MetadataProfile { } } +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum NewItemMonitorType { + #[default] + #[display_style(name = "All Albums")] + All, + #[display_style(name = "No New Albums")] + None, + #[display_style(name = "New Albums")] + New, +} + #[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DownloadRecord { @@ -174,14 +203,15 @@ impl From for Serdeable { serde_enum_from!( LidarrSerdeable { + Artist(Artist), Artists(Vec), - DiskSpaces(Vec), + DiskSpaces(Vec), DownloadsResponse(DownloadsResponse), MetadataProfiles(Vec), - QualityProfiles(Vec), - RootFolders(Vec), + QualityProfiles(Vec), + RootFolders(Vec), SystemStatus(SystemStatus), - Tags(Vec), + Tags(Vec), Value(Value), } ); diff --git a/src/models/lidarr_models_tests.rs b/src/models/lidarr_models_tests.rs index 89b8996..e8f7ea3 100644 --- a/src/models/lidarr_models_tests.rs +++ b/src/models/lidarr_models_tests.rs @@ -1,8 +1,14 @@ #[cfg(test)] mod tests { + use chrono::Utc; use pretty_assertions::{assert_eq, assert_str_eq}; use serde_json::json; + use crate::models::lidarr_models::{ + DownloadRecord, DownloadStatus, DownloadsResponse, MetadataProfile, NewItemMonitorType, + SystemStatus, + }; + use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag}; use crate::models::{ Serdeable, lidarr_models::{Artist, ArtistStatistics, ArtistStatus, LidarrSerdeable, Ratings}, @@ -13,6 +19,20 @@ mod tests { assert_eq!(ArtistStatus::default(), ArtistStatus::Continuing); } + #[test] + fn test_new_item_monitor_type_display() { + assert_str_eq!(NewItemMonitorType::All.to_string(), "all"); + assert_str_eq!(NewItemMonitorType::None.to_string(), "none"); + assert_str_eq!(NewItemMonitorType::New.to_string(), "new"); + } + + #[test] + fn test_new_item_monitor_type_to_display_str() { + assert_str_eq!(NewItemMonitorType::All.to_display_str(), "All Albums"); + assert_str_eq!(NewItemMonitorType::None.to_display_str(), "No New Albums"); + assert_str_eq!(NewItemMonitorType::New.to_display_str(), "New Albums"); + } + #[test] fn test_lidarr_serdeable_from() { let lidarr_serdeable = LidarrSerdeable::Value(json!({})); @@ -65,6 +85,7 @@ mod tests { "qualityProfileId": 1, "metadataProfileId": 1, "monitored": true, + "monitorNewItems": "all", "genres": ["Rock", "Alternative"], "tags": [1, 2], "added": "2023-01-01T00:00:00Z", @@ -95,6 +116,7 @@ mod tests { assert_eq!(artist.quality_profile_id, 1); assert_eq!(artist.metadata_profile_id, 1); assert!(artist.monitored); + assert_eq!(artist.monitor_new_items, NewItemMonitorType::All); assert_eq!(artist.genres, vec!["Rock", "Alternative"]); assert_eq!(artist.tags.len(), 2); assert_some!(&artist.ratings); @@ -184,6 +206,7 @@ mod tests { "qualityProfileId": 1, "metadataProfileId": 1, "monitored": false, + "monitorNewItems": "all", "genres": [], "tags": [], "added": "2023-01-01T00:00:00Z" @@ -194,7 +217,169 @@ mod tests { assert_none!(&artist.overview); assert_none!(&artist.artist_type); assert_none!(&artist.disambiguation); + assert_eq!(artist.monitor_new_items, NewItemMonitorType::All); assert_none!(&artist.ratings); assert_none!(&artist.statistics); } + + #[test] + fn test_lidarr_serdeable_from_artist() { + let artist = Artist { + id: 1, + ..Artist::default() + }; + + let lidarr_serdeable: LidarrSerdeable = artist.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Artist(artist)); + } + + #[test] + fn test_lidarr_serdeable_from_disk_spaces() { + let disk_spaces = vec![DiskSpace { + free_space: 1, + total_space: 1, + }]; + + let lidarr_serdeable: LidarrSerdeable = disk_spaces.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::DiskSpaces(disk_spaces)); + } + + #[test] + fn test_lidarr_serdeable_from_downloads_response() { + let downloads_response = DownloadsResponse { + records: vec![DownloadRecord { + id: 1, + ..DownloadRecord::default() + }], + }; + + let lidarr_serdeable: LidarrSerdeable = downloads_response.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::DownloadsResponse(downloads_response) + ); + } + + #[test] + fn test_lidarr_serdeable_from_metadata_profiles() { + let metadata_profiles = vec![MetadataProfile { + id: 1, + name: "Standard".to_owned(), + }]; + + let lidarr_serdeable: LidarrSerdeable = metadata_profiles.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::MetadataProfiles(metadata_profiles) + ); + } + + #[test] + fn test_lidarr_serdeable_from_quality_profiles() { + let quality_profiles = vec![QualityProfile { + id: 1, + name: "Any".to_owned(), + }]; + + let lidarr_serdeable: LidarrSerdeable = quality_profiles.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::QualityProfiles(quality_profiles) + ); + } + + #[test] + fn test_lidarr_serdeable_from_root_folders() { + let root_folders = vec![RootFolder { + id: 1, + path: "/music".to_owned(), + accessible: true, + free_space: 1000000, + unmapped_folders: None, + }]; + + let lidarr_serdeable: LidarrSerdeable = root_folders.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::RootFolders(root_folders)); + } + + #[test] + fn test_lidarr_serdeable_from_system_status() { + let system_status = SystemStatus { + version: "1.0.0".to_owned(), + start_time: Utc::now(), + }; + + let lidarr_serdeable: LidarrSerdeable = system_status.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::SystemStatus(system_status) + ); + } + + #[test] + fn test_lidarr_serdeable_from_tags() { + let tags = vec![Tag { + id: 1, + label: "rock".to_owned(), + }]; + + let lidarr_serdeable: LidarrSerdeable = tags.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Tags(tags)); + } + + #[test] + fn test_artist_status_display() { + assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing"); + assert_str_eq!(ArtistStatus::Ended.to_string(), "ended"); + assert_str_eq!(ArtistStatus::Deleted.to_string(), "deleted"); + } + + #[test] + fn test_artist_status_to_display_str() { + assert_str_eq!(ArtistStatus::Continuing.to_display_str(), "Continuing"); + assert_str_eq!(ArtistStatus::Ended.to_display_str(), "Ended"); + assert_str_eq!(ArtistStatus::Deleted.to_display_str(), "Deleted"); + } + + #[test] + fn test_download_status_display() { + assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown"); + assert_str_eq!(DownloadStatus::Queued.to_string(), "queued"); + assert_str_eq!(DownloadStatus::Paused.to_string(), "paused"); + assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading"); + assert_str_eq!(DownloadStatus::Completed.to_string(), "completed"); + assert_str_eq!(DownloadStatus::Failed.to_string(), "failed"); + assert_str_eq!(DownloadStatus::Warning.to_string(), "warning"); + assert_str_eq!(DownloadStatus::Delay.to_string(), "delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_string(), + "downloadClientUnavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback"); + } + + #[test] + fn test_download_status_to_display_str() { + assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued"); + assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused"); + assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading"); + assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed"); + assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed"); + assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning"); + assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_display_str(), + "Download Client Unavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback"); + } } diff --git a/src/network/lidarr_network/library/lidarr_library_network_tests.rs b/src/network/lidarr_network/library/lidarr_library_network_tests.rs index 9d6bc36..4944a8c 100644 --- a/src/network/lidarr_network/library/lidarr_library_network_tests.rs +++ b/src/network/lidarr_network/library/lidarr_library_network_tests.rs @@ -3,6 +3,7 @@ mod tests { use crate::models::lidarr_models::{Artist, DeleteArtistParams, LidarrSerdeable}; use crate::network::lidarr_network::LidarrEvent; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use mockito::Matcher; use pretty_assertions::assert_eq; use serde_json::json; @@ -18,6 +19,7 @@ mod tests { "qualityProfileId": 1, "metadataProfileId": 1, "monitored": true, + "monitorNewItems": "all", "genres": [], "tags": [], "added": "2023-01-01T00:00:00Z" @@ -66,4 +68,88 @@ mod tests { async_server.assert_async().await; } + + #[tokio::test] + async fn test_handle_get_artist_details_event() { + let artist_json = json!({ + "id": 1, + "mbId": "test-mb-id", + "artistName": "Test Artist", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "path": "/music/test-artist", + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": [], + "tags": [], + "added": "2023-01-01T00:00:00Z" + }); + let response: Artist = serde_json::from_value(artist_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(artist_json) + .path("/1") + .build_for(LidarrEvent::GetArtistDetails(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetArtistDetails(1)) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::Artist(artist) = result.unwrap() else { + panic!("Expected Artist"); + }; + + assert_eq!(artist, response); + } + + #[tokio::test] + async fn test_handle_toggle_artist_monitoring_event() { + let artist_json = json!({ + "id": 1, + "mbId": "test-mb-id", + "artistName": "Test Artist", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "path": "/music/test-artist", + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": [], + "tags": [], + "added": "2023-01-01T00:00:00Z" + }); + let mut expected_body = artist_json.clone(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + let (get_mock, app, mut server) = MockServarrApi::get() + .returns(artist_json) + .path("/1") + .build_for(LidarrEvent::GetArtistDetails(1)) + .await; + let put_mock = server + .mock("PUT", "/api/v1/artist/1") + .match_body(Matcher::Json(expected_body)) + .match_header("X-Api-Key", "test1234") + .with_status(202) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::ToggleArtistMonitoring(1)) + .await + .is_ok() + ); + + get_mock.assert_async().await; + put_mock.assert_async().await; + } } diff --git a/src/network/lidarr_network/library/mod.rs b/src/network/lidarr_network/library/mod.rs index 35d6f3c..98987c6 100644 --- a/src/network/lidarr_network/library/mod.rs +++ b/src/network/lidarr_network/library/mod.rs @@ -1,5 +1,6 @@ use anyhow::Result; -use log::info; +use log::{debug, info, warn}; +use serde_json::{Value, json}; use crate::models::Route; use crate::models::lidarr_models::{Artist, DeleteArtistParams}; @@ -65,4 +66,89 @@ impl Network<'_, '_> { }) .await } + + pub(in crate::network::lidarr_network) async fn get_artist_details( + &mut self, + artist_id: i64, + ) -> Result { + info!("Fetching details for Lidarr artist with ID: {artist_id}"); + let event = LidarrEvent::GetArtistDetails(artist_id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + Some(format!("/{artist_id}")), + None, + ) + .await; + + self + .handle_request::<(), Artist>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn toggle_artist_monitoring( + &mut self, + artist_id: i64, + ) -> Result<()> { + let event = LidarrEvent::ToggleArtistMonitoring(artist_id); + + let detail_event = LidarrEvent::GetArtistDetails(artist_id); + info!("Toggling artist monitoring for artist with ID: {artist_id}"); + info!("Fetching artist details for artist with ID: {artist_id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{artist_id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_artist_body, _| { + response = detailed_artist_body.to_string() + }) + .await?; + + info!("Constructing toggle artist monitoring body"); + + match serde_json::from_str::(&response) { + Ok(mut detailed_artist_body) => { + let monitored = detailed_artist_body + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); + + *detailed_artist_body.get_mut("monitored").unwrap() = json!(!monitored); + + debug!("Toggle artist monitoring body: {detailed_artist_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_artist_body), + Some(format!("/{artist_id}")), + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + Err(_) => { + warn!("Request for detailed artist body was interrupted"); + Ok(()) + } + } + } } diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index fbf7c08..f97f4e5 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -8,6 +8,18 @@ mod tests { use rstest::rstest; use serde_json::json; + #[rstest] + fn test_resource_artist( + #[values( + LidarrEvent::GetArtistDetails(0), + LidarrEvent::ListArtists, + LidarrEvent::ToggleArtistMonitoring(0) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/artist"); + } + #[rstest] #[case(LidarrEvent::GetDiskSpace, "/diskspace")] #[case(LidarrEvent::GetDownloads(500), "/queue")] @@ -17,7 +29,6 @@ mod tests { #[case(LidarrEvent::GetStatus, "/system/status")] #[case(LidarrEvent::GetTags, "/tag")] #[case(LidarrEvent::HealthCheck, "/health")] - #[case(LidarrEvent::ListArtists, "/artist")] fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) { assert_str_eq!(event.resource(), expected_uri); } diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index ed782ab..db62bef 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -18,6 +18,7 @@ mod lidarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone)] pub enum LidarrEvent { DeleteArtist(DeleteArtistParams), + GetArtistDetails(i64), GetDiskSpace, GetDownloads(u64), GetMetadataProfiles, @@ -27,12 +28,16 @@ pub enum LidarrEvent { GetTags, HealthCheck, ListArtists, + ToggleArtistMonitoring(i64), } impl NetworkResource for LidarrEvent { fn resource(&self) -> &'static str { match &self { - LidarrEvent::DeleteArtist(_) | LidarrEvent::ListArtists => "/artist", + LidarrEvent::DeleteArtist(_) + | LidarrEvent::GetArtistDetails(_) + | LidarrEvent::ListArtists + | LidarrEvent::ToggleArtistMonitoring(_) => "/artist", LidarrEvent::GetDiskSpace => "/diskspace", LidarrEvent::GetDownloads(_) => "/queue", LidarrEvent::GetMetadataProfiles => "/metadataprofile", @@ -60,6 +65,10 @@ impl Network<'_, '_> { LidarrEvent::DeleteArtist(params) => { self.delete_artist(params).await.map(LidarrSerdeable::from) } + LidarrEvent::GetArtistDetails(artist_id) => self + .get_artist_details(artist_id) + .await + .map(LidarrSerdeable::from), LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from), LidarrEvent::GetDownloads(count) => self .get_lidarr_downloads(count) @@ -84,6 +93,10 @@ impl Network<'_, '_> { .await .map(LidarrSerdeable::from), LidarrEvent::ListArtists => self.list_artists().await.map(LidarrSerdeable::from), + LidarrEvent::ToggleArtistMonitoring(artist_id) => self + .toggle_artist_monitoring(artist_id) + .await + .map(LidarrSerdeable::from), } }