This commit is contained in:
2026-01-07 10:45:49 -07:00
parent 9b4eda6a9d
commit 3c1634d1e3
65 changed files with 2355 additions and 100 deletions
+116 -1
View File
@@ -3,7 +3,7 @@ use log::{debug, info, warn};
use serde_json::{Value, json};
use crate::models::Route;
use crate::models::lidarr_models::{Artist, DeleteArtistParams};
use crate::models::lidarr_models::{Artist, DeleteArtistParams, EditArtistParams};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_models::CommandBody;
use crate::network::lidarr_network::LidarrEvent;
@@ -168,4 +168,119 @@ impl Network<'_, '_> {
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn edit_artist(
&mut self,
mut edit_artist_params: EditArtistParams,
) -> Result<()> {
info!("Editing Lidarr artist");
if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() {
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
edit_artist_params.tags = Some(tag_ids_vec);
}
let artist_id = edit_artist_params.artist_id;
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
let event = LidarrEvent::EditArtist(EditArtistParams::default());
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 edit artist body");
let mut detailed_artist_body: Value = serde_json::from_str(&response)?;
let (
monitored,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags,
) = {
let monitored = edit_artist_params.monitored.unwrap_or(
detailed_artist_body["monitored"]
.as_bool()
.expect("Unable to deserialize 'monitored'"),
);
let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| {
serde_json::from_value(detailed_artist_body["monitorNewItems"].clone())
.expect("Unable to deserialize 'monitorNewItems'")
});
let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| {
detailed_artist_body["qualityProfileId"]
.as_i64()
.expect("Unable to deserialize 'qualityProfileId'")
});
let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| {
detailed_artist_body["metadataProfileId"]
.as_i64()
.expect("Unable to deserialize 'metadataProfileId'")
});
let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| {
detailed_artist_body["path"]
.as_str()
.expect("Unable to deserialize 'path'")
.to_owned()
});
let tags = if edit_artist_params.clear_tags {
vec![]
} else {
edit_artist_params.tags.unwrap_or(
detailed_artist_body["tags"]
.as_array()
.expect("Unable to deserialize 'tags'")
.iter()
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
.collect(),
)
};
(
monitored,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags,
)
};
*detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored);
*detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items);
*detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
*detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id);
*detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path);
*detailed_artist_body.get_mut("tags").unwrap() = json!(tags);
debug!("Edit artist 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::<Value, ()>(request_props, |_, _| ())
.await
}
}
@@ -0,0 +1,132 @@
#[cfg(test)]
#[allow(dead_code)] // TODO: maybe remove?
pub mod test_utils {
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus, DownloadRecord, DownloadStatus, DownloadsResponse, Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus};
use crate::models::servarr_models::{QualityProfile, RootFolder, Tag};
use crate::models::HorizontallyScrollableText;
use bimap::BiMap;
use chrono::DateTime;
use serde_json::Number;
pub fn member() -> Member {
Member {
name: Some("alex".to_owned()),
instrument: Some("piano".to_owned())
}
}
pub fn ratings() -> Ratings {
Ratings {
votes: 15,
value: 8.4
}
}
pub fn artist_statistics() -> ArtistStatistics {
ArtistStatistics {
album_count: 1,
track_file_count: 15,
track_count: 15,
total_track_count: 15,
size_on_disk: 12345,
percent_of_tracks: 99.9
}
}
pub fn artist() -> Artist {
Artist {
id: 1,
artist_name: "Alex".into(),
foreign_artist_id: "test-foreign-id".to_owned(),
status: ArtistStatus::Continuing,
overview: Some("some interesting description of the artist".to_owned()),
artist_type: Some("Person".to_owned()),
disambiguation: Some("American pianist".to_owned()),
members: Some(vec![member()]),
path: "/nfs/music/test-artist".to_owned(),
quality_profile_id: quality_profile().id,
metadata_profile_id: metadata_profile().id,
monitored: true,
monitor_new_items: NewItemMonitorType::All,
genres: vec!["soundtrack".to_owned()],
tags: vec![Number::from(tag().id)],
added: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
ratings: Some(ratings()),
statistics: Some(artist_statistics())
}
}
pub fn quality_profile() -> QualityProfile {
QualityProfile {
id: 1,
name: "Lossless".to_owned()
}
}
pub fn quality_profile_map() -> BiMap<i64, String> {
let quality_profile = quality_profile();
BiMap::from_iter(vec![(quality_profile.id, quality_profile.name)])
}
pub fn metadata_profile() -> MetadataProfile {
MetadataProfile {
id: 1,
name: "Standard".to_owned()
}
}
pub fn metadata_profile_map() -> BiMap<i64, String> {
let metadata_profile = metadata_profile();
BiMap::from_iter(vec![(metadata_profile.id, metadata_profile.name)])
}
pub fn tag() -> Tag {
Tag {
id: 1,
label: "alex".to_owned()
}
}
pub fn tags_map() -> BiMap<i64, String> {
let tag = tag();
BiMap::from_iter(vec![(tag.id, tag.label)])
}
pub fn download_record() -> DownloadRecord {
DownloadRecord {
title: "Test download title".to_owned(),
status: DownloadStatus::Downloading,
id: 1,
album_id: Some(Number::from(1i64)),
artist_id: Some(Number::from(1i64)),
size: 3543348019f64,
sizeleft: 1771674009f64,
output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")),
indexer: "kickass torrents".to_owned(),
download_client: Some("transmission".to_owned())
}
}
pub fn downloads_response() -> DownloadsResponse {
DownloadsResponse {
records: vec![download_record()]
}
}
pub fn system_status() -> SystemStatus {
SystemStatus {
version: "1.0".to_owned(),
start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
}
}
pub fn root_folder() -> RootFolder {
RootFolder {
id: 1,
path: "/nfs".to_owned(),
accessible: true,
free_space: 219902325555200,
unmapped_folders: None,
}
}
}
+70 -2
View File
@@ -2,7 +2,9 @@ use anyhow::Result;
use log::info;
use super::{NetworkEvent, NetworkResource};
use crate::models::lidarr_models::{DeleteArtistParams, LidarrSerdeable, MetadataProfile};
use crate::models::lidarr_models::{
DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
};
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
@@ -15,9 +17,15 @@ mod system;
#[path = "lidarr_network_tests.rs"]
mod lidarr_network_tests;
#[cfg(test)]
#[path = "lidarr_network_test_utils.rs"]
pub mod lidarr_network_test_utils;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum LidarrEvent {
AddTag(String),
DeleteArtist(DeleteArtistParams),
EditArtist(EditArtistParams),
GetArtistDetails(i64),
GetDiskSpace,
GetDownloads(u64),
@@ -37,7 +45,9 @@ pub enum LidarrEvent {
impl NetworkResource for LidarrEvent {
fn resource(&self) -> &'static str {
match &self {
LidarrEvent::AddTag(_) | LidarrEvent::GetTags => "/tag",
LidarrEvent::DeleteArtist(_)
| LidarrEvent::EditArtist(_)
| LidarrEvent::GetArtistDetails(_)
| LidarrEvent::ListArtists
| LidarrEvent::ToggleArtistMonitoring(_) => "/artist",
@@ -49,7 +59,6 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::GetQualityProfiles => "/qualityprofile",
LidarrEvent::GetRootFolders => "/rootfolder",
LidarrEvent::GetStatus => "/system/status",
LidarrEvent::GetTags => "/tag",
LidarrEvent::HealthCheck => "/health",
}
}
@@ -67,6 +76,7 @@ impl Network<'_, '_> {
lidarr_event: LidarrEvent,
) -> Result<LidarrSerdeable> {
match lidarr_event {
LidarrEvent::AddTag(tag) => self.add_lidarr_tag(tag).await.map(LidarrSerdeable::from),
LidarrEvent::DeleteArtist(params) => {
self.delete_artist(params).await.map(LidarrSerdeable::from)
}
@@ -111,6 +121,7 @@ impl Network<'_, '_> {
.await
.map(LidarrSerdeable::from),
LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from),
LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from),
}
}
@@ -180,4 +191,61 @@ impl Network<'_, '_> {
})
.await
}
async fn add_lidarr_tag(&mut self, tag: String) -> Result<Tag> {
info!("Adding a new Lidarr tag");
let event = LidarrEvent::AddTag(String::new());
let request_props = self
.request_props_from(
event,
RequestMethod::Post,
Some(serde_json::json!({ "label": tag })),
None,
None,
)
.await;
self
.handle_request::<serde_json::Value, Tag>(request_props, |tag, mut app| {
app.data.lidarr_data.tags_map.insert(tag.id, tag.label);
})
.await
}
pub(in crate::network::lidarr_network) async fn extract_and_add_lidarr_tag_ids_vec(
&mut self,
edit_tags: &str,
) -> Vec<i64> {
let missing_tags_vec = {
let tags_map = &self.app.lock().await.data.lidarr_data.tags_map;
edit_tags
.split(',')
.filter(|&tag| {
!tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none()
})
.collect::<Vec<&str>>()
};
for tag in missing_tags_vec {
self
.add_lidarr_tag(tag.trim().to_owned())
.await
.expect("Unable to add tag");
}
let app = self.app.lock().await;
edit_tags
.split(',')
.filter(|tag| !tag.is_empty())
.map(|tag| {
*app
.data
.lidarr_data
.tags_map
.get_by_right(tag.to_lowercase().trim())
.unwrap()
})
.collect()
}
}
@@ -284,6 +284,7 @@ pub mod test_utils {
subtitles: Some("English".to_owned()),
}
}
pub fn quality() -> Quality {
Quality {
name: "Bluray-1080p".to_owned(),