feat: Full support for deleting an artist via CLI and TUI

This commit is contained in:
2026-01-05 15:44:51 -07:00
parent bc3aeefa6e
commit 6771a0ab38
43 changed files with 1995 additions and 332 deletions
@@ -0,0 +1,43 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{DownloadsResponse, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use pretty_assertions::assert_eq;
use serde_json::json;
#[tokio::test]
async fn test_handle_get_downloads_event() {
let downloads_json = json!({
"records": [{
"title": "Test Album",
"status": "downloading",
"id": 1,
"size": 100.0,
"sizeleft": 50.0,
"indexer": "test-indexer"
}]
});
let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(downloads_json)
.query("pageSize=500")
.build_for(LidarrEvent::GetDownloads(500))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetDownloads(500))
.await;
mock.assert_async().await;
let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else {
panic!("Expected DownloadsResponse");
};
assert_eq!(downloads_response, response);
assert!(!app.lock().await.data.lidarr_data.downloads.is_empty());
}
}
@@ -0,0 +1,40 @@
use anyhow::Result;
use log::info;
use crate::models::lidarr_models::DownloadsResponse;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
#[cfg(test)]
#[path = "lidarr_downloads_network_tests.rs"]
mod lidarr_downloads_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn get_lidarr_downloads(
&mut self,
count: u64,
) -> Result<DownloadsResponse> {
info!("Fetching Lidarr downloads");
let event = LidarrEvent::GetDownloads(count);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("pageSize={count}")),
)
.await;
self
.handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| {
app
.data
.lidarr_data
.downloads
.set_items(queue_response.records);
})
.await
}
}
@@ -1,6 +1,6 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{Artist, LidarrSerdeable};
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 pretty_assertions::assert_eq;
@@ -41,4 +41,29 @@ mod tests {
assert_eq!(artists, response);
assert!(!app.lock().await.data.lidarr_data.artists.is_empty());
}
#[tokio::test]
async fn test_handle_delete_artist_event() {
let delete_artist_params = DeleteArtistParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
};
let (async_server, app, _server) = MockServarrApi::delete()
.path("/1")
.query("deleteFiles=true&addImportListExclusion=true")
.build_for(LidarrEvent::DeleteArtist(delete_artist_params.clone()))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::DeleteArtist(delete_artist_params))
.await
.is_ok()
);
async_server.assert_async().await;
}
}
+33 -1
View File
@@ -1,7 +1,7 @@
use anyhow::Result;
use log::info;
use crate::models::lidarr_models::Artist;
use crate::models::lidarr_models::{Artist, DeleteArtistParams};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::Route;
use crate::network::lidarr_network::LidarrEvent;
@@ -12,6 +12,38 @@ use crate::network::{Network, RequestMethod};
mod lidarr_library_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn delete_artist(
&mut self,
delete_artist_params: DeleteArtistParams,
) -> Result<()> {
let event = LidarrEvent::DeleteArtist(DeleteArtistParams::default());
let DeleteArtistParams {
id,
delete_files,
add_import_list_exclusion,
} = delete_artist_params;
info!(
"Deleting Lidarr artist with ID: {id} with deleteFiles={delete_files} and addImportListExclusion={add_import_list_exclusion}"
);
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{id}")),
Some(format!(
"deleteFiles={delete_files}&addImportListExclusion={add_import_list_exclusion}"
)),
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn list_artists(&mut self) -> Result<Vec<Artist>> {
info!("Fetching Lidarr artists");
let event = LidarrEvent::ListArtists;
@@ -1,8 +1,12 @@
#[cfg(test)]
mod tests {
use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent};
use pretty_assertions::assert_str_eq;
use crate::models::lidarr_models::{LidarrSerdeable, MetadataProfile};
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::{lidarr_network::LidarrEvent, NetworkEvent, NetworkResource};
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::json;
#[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
@@ -25,4 +29,109 @@ mod tests {
NetworkEvent::from(LidarrEvent::HealthCheck)
);
}
#[tokio::test]
async fn test_handle_get_metadata_profiles_event() {
let metadata_profiles_json = json!([{
"id": 1,
"name": "Standard"
}]);
let response: Vec<MetadataProfile> =
serde_json::from_value(metadata_profiles_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(metadata_profiles_json)
.build_for(LidarrEvent::GetMetadataProfiles)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetMetadataProfiles)
.await;
mock.assert_async().await;
let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else {
panic!("Expected MetadataProfiles");
};
assert_eq!(metadata_profiles, response);
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.metadata_profile_map
.get_by_left(&1),
Some(&"Standard".to_owned())
);
}
#[tokio::test]
async fn test_handle_get_quality_profiles_event() {
let quality_profiles_json = json!([{
"id": 1,
"name": "Lossless"
}]);
let response: Vec<QualityProfile> =
serde_json::from_value(quality_profiles_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(quality_profiles_json)
.build_for(LidarrEvent::GetQualityProfiles)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetQualityProfiles)
.await;
mock.assert_async().await;
let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else {
panic!("Expected QualityProfiles");
};
assert_eq!(quality_profiles, response);
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.quality_profile_map
.get_by_left(&1),
Some(&"Lossless".to_owned())
);
}
#[tokio::test]
async fn test_handle_get_tags_event() {
let tags_json = json!([{
"id": 1,
"label": "usenet"
}]);
let response: Vec<Tag> = serde_json::from_value(tags_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(tags_json)
.build_for(LidarrEvent::GetTags)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network.handle_lidarr_event(LidarrEvent::GetTags).await;
mock.assert_async().await;
let LidarrSerdeable::Tags(tags) = result.unwrap() else {
panic!("Expected Tags");
};
assert_eq!(tags, response);
assert_eq!(
app.lock().await.data.lidarr_data.tags_map.get_by_left(&1),
Some(&"usenet".to_owned())
);
}
}
+65 -3
View File
@@ -1,10 +1,14 @@
use anyhow::Result;
use log::info;
use super::{NetworkEvent, NetworkResource};
use crate::models::lidarr_models::LidarrSerdeable;
use crate::network::Network;
use crate::models::lidarr_models::{DeleteArtistParams, LidarrSerdeable, MetadataProfile};
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
mod downloads;
mod library;
mod root_folders;
mod system;
#[cfg(test)]
@@ -13,6 +17,7 @@ mod lidarr_network_tests;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum LidarrEvent {
DeleteArtist(DeleteArtistParams),
GetDiskSpace,
GetDownloads(u64),
GetMetadataProfiles,
@@ -27,6 +32,7 @@ pub enum LidarrEvent {
impl NetworkResource for LidarrEvent {
fn resource(&self) -> &'static str {
match &self {
LidarrEvent::DeleteArtist(_) | LidarrEvent::ListArtists => "/artist",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) => "/queue",
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
@@ -35,7 +41,6 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::GetStatus => "/system/status",
LidarrEvent::GetTags => "/tag",
LidarrEvent::HealthCheck => "/health",
LidarrEvent::ListArtists => "/artist",
}
}
}
@@ -52,6 +57,9 @@ impl Network<'_, '_> {
lidarr_event: LidarrEvent,
) -> Result<LidarrSerdeable> {
match lidarr_event {
LidarrEvent::DeleteArtist(params) => {
self.delete_artist(params).await.map(LidarrSerdeable::from)
}
LidarrEvent::GetDiskSpace => self
.get_lidarr_diskspace()
.await
@@ -84,4 +92,58 @@ impl Network<'_, '_> {
LidarrEvent::ListArtists => self.list_artists().await.map(LidarrSerdeable::from),
}
}
async fn get_lidarr_metadata_profiles(&mut self) -> Result<Vec<MetadataProfile>> {
info!("Fetching Lidarr metadata profiles");
let event = LidarrEvent::GetMetadataProfiles;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<MetadataProfile>>(request_props, |metadata_profiles, mut app| {
app.data.lidarr_data.metadata_profile_map = metadata_profiles
.into_iter()
.map(|profile| (profile.id, profile.name))
.collect();
})
.await
}
async fn get_lidarr_quality_profiles(&mut self) -> Result<Vec<QualityProfile>> {
info!("Fetching Lidarr quality profiles");
let event = LidarrEvent::GetQualityProfiles;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
app.data.lidarr_data.quality_profile_map = quality_profiles
.into_iter()
.map(|profile| (profile.id, profile.name))
.collect();
})
.await
}
async fn get_lidarr_tags(&mut self) -> Result<Vec<Tag>> {
info!("Fetching Lidarr tags");
let event = LidarrEvent::GetTags;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
app.data.lidarr_data.tags_map = tags_vec
.into_iter()
.map(|tag| (tag.id, tag.label))
.collect();
})
.await
}
}
@@ -0,0 +1,39 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::servarr_models::RootFolder;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use pretty_assertions::assert_eq;
use serde_json::json;
#[tokio::test]
async fn test_handle_get_root_folders_event() {
let root_folders_json = json!([{
"id": 1,
"path": "/music",
"accessible": true,
"freeSpace": 50000000000i64
}]);
let response: Vec<RootFolder> = serde_json::from_value(root_folders_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(root_folders_json)
.build_for(LidarrEvent::GetRootFolders)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetRootFolders)
.await;
mock.assert_async().await;
let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else {
panic!("Expected RootFolders");
};
assert_eq!(root_folders, response);
assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty());
}
}
@@ -0,0 +1,29 @@
use anyhow::Result;
use log::info;
use crate::models::servarr_models::RootFolder;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
#[cfg(test)]
#[path = "lidarr_root_folders_network_tests.rs"]
mod lidarr_root_folders_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders(
&mut self,
) -> Result<Vec<RootFolder>> {
info!("Fetching Lidarr root folders");
let event = LidarrEvent::GetRootFolders;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
app.data.lidarr_data.root_folders.set_items(root_folders);
})
.await
}
}
@@ -1,9 +1,7 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{
DownloadsResponse, LidarrSerdeable, MetadataProfile, SystemStatus,
};
use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag};
use crate::models::lidarr_models::{LidarrSerdeable, SystemStatus};
use crate::models::servarr_models::DiskSpace;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use pretty_assertions::assert_eq;
@@ -22,111 +20,6 @@ mod tests {
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_metadata_profiles_event() {
let metadata_profiles_json = json!([{
"id": 1,
"name": "Standard"
}]);
let response: Vec<MetadataProfile> =
serde_json::from_value(metadata_profiles_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(metadata_profiles_json)
.build_for(LidarrEvent::GetMetadataProfiles)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetMetadataProfiles)
.await;
mock.assert_async().await;
let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else {
panic!("Expected MetadataProfiles");
};
assert_eq!(metadata_profiles, response);
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.metadata_profile_map
.get_by_left(&1),
Some(&"Standard".to_owned())
);
}
#[tokio::test]
async fn test_handle_get_quality_profiles_event() {
let quality_profiles_json = json!([{
"id": 1,
"name": "Lossless"
}]);
let response: Vec<QualityProfile> =
serde_json::from_value(quality_profiles_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(quality_profiles_json)
.build_for(LidarrEvent::GetQualityProfiles)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetQualityProfiles)
.await;
mock.assert_async().await;
let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else {
panic!("Expected QualityProfiles");
};
assert_eq!(quality_profiles, response);
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.quality_profile_map
.get_by_left(&1),
Some(&"Lossless".to_owned())
);
}
#[tokio::test]
async fn test_handle_get_tags_event() {
let tags_json = json!([{
"id": 1,
"label": "usenet"
}]);
let response: Vec<Tag> = serde_json::from_value(tags_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(tags_json)
.build_for(LidarrEvent::GetTags)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network.handle_lidarr_event(LidarrEvent::GetTags).await;
mock.assert_async().await;
let LidarrSerdeable::Tags(tags) = result.unwrap() else {
panic!("Expected Tags");
};
assert_eq!(tags, response);
assert_eq!(
app.lock().await.data.lidarr_data.tags_map.get_by_left(&1),
Some(&"usenet".to_owned())
);
}
#[tokio::test]
async fn test_handle_get_diskspace_event() {
let diskspace_json = json!([{
@@ -153,71 +46,6 @@ mod tests {
assert!(!app.lock().await.data.lidarr_data.disk_space_vec.is_empty());
}
#[tokio::test]
async fn test_handle_get_downloads_event() {
let downloads_json = json!({
"records": [{
"title": "Test Album",
"status": "downloading",
"id": 1,
"size": 100.0,
"sizeleft": 50.0,
"indexer": "test-indexer"
}]
});
let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(downloads_json)
.query("pageSize=500")
.build_for(LidarrEvent::GetDownloads(500))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetDownloads(500))
.await;
mock.assert_async().await;
let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else {
panic!("Expected DownloadsResponse");
};
assert_eq!(downloads_response, response);
assert!(!app.lock().await.data.lidarr_data.downloads.is_empty());
}
#[tokio::test]
async fn test_handle_get_root_folders_event() {
let root_folders_json = json!([{
"id": 1,
"path": "/music",
"accessible": true,
"freeSpace": 50000000000i64
}]);
let response: Vec<RootFolder> = serde_json::from_value(root_folders_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(root_folders_json)
.build_for(LidarrEvent::GetRootFolders)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetRootFolders)
.await;
mock.assert_async().await;
let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else {
panic!("Expected RootFolders");
};
assert_eq!(root_folders, response);
assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty());
}
#[tokio::test]
async fn test_handle_get_status_event() {
let status_json = json!({
+2 -105
View File
@@ -1,8 +1,8 @@
use anyhow::Result;
use log::info;
use crate::models::lidarr_models::{DownloadsResponse, MetadataProfile, SystemStatus};
use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag};
use crate::models::lidarr_models::SystemStatus;
use crate::models::servarr_models::DiskSpace;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
@@ -24,64 +24,6 @@ impl Network<'_, '_> {
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_metadata_profiles(
&mut self,
) -> Result<Vec<MetadataProfile>> {
info!("Fetching Lidarr metadata profiles");
let event = LidarrEvent::GetMetadataProfiles;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<MetadataProfile>>(request_props, |metadata_profiles, mut app| {
app.data.lidarr_data.metadata_profile_map = metadata_profiles
.into_iter()
.map(|profile| (profile.id, profile.name))
.collect();
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_quality_profiles(
&mut self,
) -> Result<Vec<QualityProfile>> {
info!("Fetching Lidarr quality profiles");
let event = LidarrEvent::GetQualityProfiles;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
app.data.lidarr_data.quality_profile_map = quality_profiles
.into_iter()
.map(|profile| (profile.id, profile.name))
.collect();
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_tags(&mut self) -> Result<Vec<Tag>> {
info!("Fetching Lidarr tags");
let event = LidarrEvent::GetTags;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
app.data.lidarr_data.tags_map = tags_vec
.into_iter()
.map(|tag| (tag.id, tag.label))
.collect();
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_diskspace(
&mut self,
) -> Result<Vec<DiskSpace>> {
@@ -99,51 +41,6 @@ impl Network<'_, '_> {
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_downloads(
&mut self,
count: u64,
) -> Result<DownloadsResponse> {
info!("Fetching Lidarr downloads");
let event = LidarrEvent::GetDownloads(count);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("pageSize={count}")),
)
.await;
self
.handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| {
app
.data
.lidarr_data
.downloads
.set_items(queue_response.records);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders(
&mut self,
) -> Result<Vec<RootFolder>> {
info!("Fetching Lidarr root folders");
let event = LidarrEvent::GetRootFolders;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
app.data.lidarr_data.root_folders.set_items(root_folders);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_status(
&mut self,
) -> Result<SystemStatus> {