feat: Full CLI and TUI support for adding an artist to Lidarr

This commit is contained in:
2026-01-08 15:16:01 -07:00
parent e94f78dc7b
commit c624d1b9e4
28 changed files with 3448 additions and 86 deletions
+57
View File
@@ -134,6 +134,40 @@ pub enum NewItemMonitorType {
New,
}
#[derive(
Serialize,
Deserialize,
Default,
PartialEq,
Eq,
Clone,
Copy,
Debug,
EnumIter,
clap::ValueEnum,
Display,
EnumDisplayStyle,
)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum MonitorType {
#[default]
#[display_style(name = "All Albums")]
All,
#[display_style(name = "Future Albums")]
Future,
#[display_style(name = "Missing Albums")]
Missing,
#[display_style(name = "Existing Albums")]
Existing,
#[display_style(name = "First Album")]
First,
#[display_style(name = "Latest Album")]
Latest,
None,
Unknown,
}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DownloadRecord {
@@ -219,6 +253,29 @@ pub struct DeleteArtistParams {
pub add_import_list_exclusion: bool,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddArtistBody {
pub foreign_artist_id: String,
pub artist_name: String,
pub monitored: bool,
pub root_folder_path: String,
pub quality_profile_id: i64,
pub metadata_profile_id: i64,
pub tags: Vec<i64>,
#[serde(skip_serializing, skip_deserializing)]
pub tag_input_string: Option<String>,
pub add_options: AddArtistOptions,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddArtistOptions {
pub monitor: MonitorType,
pub monitor_new_items: NewItemMonitorType,
pub search_for_missing_albums: bool,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditArtistParams {
+25 -1
View File
@@ -6,7 +6,7 @@ mod tests {
use crate::models::lidarr_models::{
AddArtistSearchResult, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
MetadataProfile, NewItemMonitorType, SystemStatus,
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
};
use crate::models::servarr_models::{
DiskSpace, HostConfig, QualityProfile, RootFolder, SecurityConfig, Tag,
@@ -35,6 +35,30 @@ mod tests {
assert_str_eq!(NewItemMonitorType::New.to_display_str(), "New Albums");
}
#[test]
fn test_monitor_type_display() {
assert_str_eq!(MonitorType::All.to_string(), "all");
assert_str_eq!(MonitorType::Future.to_string(), "future");
assert_str_eq!(MonitorType::Missing.to_string(), "missing");
assert_str_eq!(MonitorType::Existing.to_string(), "existing");
assert_str_eq!(MonitorType::First.to_string(), "first");
assert_str_eq!(MonitorType::Latest.to_string(), "latest");
assert_str_eq!(MonitorType::None.to_string(), "none");
assert_str_eq!(MonitorType::Unknown.to_string(), "unknown");
}
#[test]
fn test_monitor_type_to_display_str() {
assert_str_eq!(MonitorType::All.to_display_str(), "All Albums");
assert_str_eq!(MonitorType::Future.to_display_str(), "Future Albums");
assert_str_eq!(MonitorType::Missing.to_display_str(), "Missing Albums");
assert_str_eq!(MonitorType::Existing.to_display_str(), "Existing Albums");
assert_str_eq!(MonitorType::First.to_display_str(), "First Album");
assert_str_eq!(MonitorType::Latest.to_display_str(), "Latest Album");
assert_str_eq!(MonitorType::None.to_display_str(), "None");
assert_str_eq!(MonitorType::Unknown.to_display_str(), "Unknown");
}
#[test]
fn test_lidarr_serdeable_from() {
let lidarr_serdeable = LidarrSerdeable::Value(json!({}));
+52 -2
View File
@@ -1,6 +1,6 @@
use serde_json::Number;
use super::modals::EditArtistModal;
use super::modals::{AddArtistModal, EditArtistModal};
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
@@ -31,6 +31,7 @@ use {
mod lidarr_data_tests;
pub struct LidarrData<'a> {
pub add_artist_modal: Option<AddArtistModal>,
pub add_artist_search: Option<HorizontallyScrollableText>,
pub add_import_list_exclusion: bool,
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
@@ -92,6 +93,7 @@ impl LidarrData<'_> {
impl<'a> Default for LidarrData<'a> {
fn default() -> LidarrData<'a> {
LidarrData {
add_artist_modal: None,
add_artist_search: None,
add_import_list_exclusion: false,
add_searched_artists: None,
@@ -122,6 +124,25 @@ impl<'a> Default for LidarrData<'a> {
#[cfg(test)]
impl LidarrData<'_> {
pub fn test_default_fully_populated() -> Self {
let mut add_artist_modal = AddArtistModal {
tags: "usenet, testing".into(),
..AddArtistModal::default()
};
add_artist_modal
.monitor_list
.set_items(Vec::from_iter(MonitorType::iter()));
add_artist_modal
.monitor_new_items_list
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
add_artist_modal
.metadata_profile_list
.set_items(vec![metadata_profile().name]);
add_artist_modal
.quality_profile_list
.set_items(vec![quality_profile().name]);
add_artist_modal
.root_folder_list
.set_items(vec![root_folder()]);
let mut edit_artist_modal = EditArtistModal {
monitored: Some(true),
path: "/nfs/music".into(),
@@ -144,6 +165,7 @@ impl LidarrData<'_> {
quality_profile_map: quality_profile_map(),
metadata_profile_map: metadata_profile_map(),
edit_artist_modal: Some(edit_artist_modal),
add_artist_modal: Some(add_artist_modal),
tags_map: tags_map(),
..LidarrData::default()
};
@@ -172,9 +194,18 @@ pub enum ActiveLidarrBlock {
#[default]
Artists,
ArtistsSortPrompt,
AddArtistAlreadyInLibrary,
AddArtistConfirmPrompt,
AddArtistEmptySearchResults,
AddArtistPrompt,
AddArtistSearchInput,
AddArtistSearchResults,
AddArtistSelectMetadataProfile,
AddArtistSelectMonitor,
AddArtistSelectMonitorNewItems,
AddArtistSelectQualityProfile,
AddArtistSelectRootFolder,
AddArtistTagsInput,
DeleteArtistPrompt,
DeleteArtistConfirmPrompt,
DeleteArtistToggleDeleteFile,
@@ -204,10 +235,29 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::UpdateAllArtistsPrompt,
];
pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 3] = [
pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 12] = [
ActiveLidarrBlock::AddArtistAlreadyInLibrary,
ActiveLidarrBlock::AddArtistConfirmPrompt,
ActiveLidarrBlock::AddArtistEmptySearchResults,
ActiveLidarrBlock::AddArtistPrompt,
ActiveLidarrBlock::AddArtistSearchInput,
ActiveLidarrBlock::AddArtistSearchResults,
ActiveLidarrBlock::AddArtistSelectMetadataProfile,
ActiveLidarrBlock::AddArtistSelectMonitor,
ActiveLidarrBlock::AddArtistSelectMonitorNewItems,
ActiveLidarrBlock::AddArtistSelectQualityProfile,
ActiveLidarrBlock::AddArtistSelectRootFolder,
ActiveLidarrBlock::AddArtistTagsInput,
];
pub const ADD_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::AddArtistSelectRootFolder],
&[ActiveLidarrBlock::AddArtistSelectMonitor],
&[ActiveLidarrBlock::AddArtistSelectMonitorNewItems],
&[ActiveLidarrBlock::AddArtistSelectQualityProfile],
&[ActiveLidarrBlock::AddArtistSelectMetadataProfile],
&[ActiveLidarrBlock::AddArtistTagsInput],
&[ActiveLidarrBlock::AddArtistConfirmPrompt],
];
pub static DELETE_ARTIST_BLOCKS: [ActiveLidarrBlock; 4] = [
@@ -2,8 +2,8 @@
mod tests {
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_BLOCKS,
EDIT_ARTIST_SELECTION_BLOCKS,
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS,
DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
};
use crate::models::{
BlockSelectionState, Route,
@@ -155,10 +155,54 @@ mod tests {
#[test]
fn test_add_artist_blocks_contents() {
assert_eq!(ADD_ARTIST_BLOCKS.len(), 3);
assert_eq!(ADD_ARTIST_BLOCKS.len(), 12);
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistAlreadyInLibrary));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistConfirmPrompt));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistEmptySearchResults));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistPrompt));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchInput));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchResults));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMetadataProfile));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMonitor));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMonitorNewItems));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectQualityProfile));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectRootFolder));
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistTagsInput));
}
#[test]
fn test_add_artist_selection_blocks_ordering() {
let mut add_artist_block_iter = ADD_ARTIST_SELECTION_BLOCKS.iter();
assert_eq!(
add_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::AddArtistSelectRootFolder]
);
assert_eq!(
add_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::AddArtistSelectMonitor]
);
assert_eq!(
add_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::AddArtistSelectMonitorNewItems]
);
assert_eq!(
add_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::AddArtistSelectQualityProfile]
);
assert_eq!(
add_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::AddArtistSelectMetadataProfile]
);
assert_eq!(
add_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::AddArtistTagsInput]
);
assert_eq!(
add_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::AddArtistConfirmPrompt]
);
assert_none!(add_artist_block_iter.next());
}
#[test]
+38 -1
View File
@@ -2,13 +2,50 @@ use strum::IntoEnumIterator;
use super::lidarr_data::LidarrData;
use crate::models::{
HorizontallyScrollableText, lidarr_models::NewItemMonitorType, stateful_list::StatefulList,
HorizontallyScrollableText,
lidarr_models::{MonitorType, NewItemMonitorType},
servarr_models::RootFolder,
stateful_list::StatefulList,
};
#[cfg(test)]
#[path = "modals_tests.rs"]
mod modals_tests;
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct AddArtistModal {
pub root_folder_list: StatefulList<RootFolder>,
pub monitor_list: StatefulList<MonitorType>,
pub monitor_new_items_list: StatefulList<NewItemMonitorType>,
pub quality_profile_list: StatefulList<String>,
pub metadata_profile_list: StatefulList<String>,
pub tags: HorizontallyScrollableText,
}
impl From<&LidarrData<'_>> for AddArtistModal {
fn from(lidarr_data: &LidarrData<'_>) -> AddArtistModal {
let mut add_artist_modal = AddArtistModal::default();
add_artist_modal
.monitor_list
.set_items(Vec::from_iter(MonitorType::iter()));
add_artist_modal
.monitor_new_items_list
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
add_artist_modal
.quality_profile_list
.set_items(lidarr_data.sorted_quality_profile_names());
add_artist_modal
.metadata_profile_list
.set_items(lidarr_data.sorted_metadata_profile_names());
add_artist_modal
.root_folder_list
.set_items(lidarr_data.root_folders.items.to_vec());
add_artist_modal
}
}
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct EditArtistModal {
+59 -2
View File
@@ -3,9 +3,66 @@ mod tests {
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::models::lidarr_models::{Artist, NewItemMonitorType};
use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType};
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::models::servarr_data::lidarr::modals::{AddArtistModal, EditArtistModal};
use crate::models::servarr_models::RootFolder;
#[test]
fn test_add_artist_modal_from_lidarr_data() {
let mut lidarr_data = LidarrData {
quality_profile_map: BiMap::from_iter([
(2i64, "Lossless".to_owned()),
(1i64, "Standard".to_owned()),
]),
metadata_profile_map: BiMap::from_iter([
(2i64, "None".to_owned()),
(1i64, "Standard".to_owned()),
]),
..LidarrData::default()
};
let root_folder_1 = RootFolder {
id: 1,
path: "/nfs".to_owned(),
accessible: true,
free_space: 219902325555200,
unmapped_folders: None,
};
lidarr_data.root_folders.set_items(vec![
root_folder_1.clone(),
RootFolder {
id: 2,
path: "/nfs2".to_owned(),
accessible: true,
free_space: 21990232555520,
unmapped_folders: None,
},
]);
let add_artist_modal = AddArtistModal::from(&lidarr_data);
assert_eq!(
*add_artist_modal.monitor_list.current_selection(),
MonitorType::default()
);
assert_eq!(
*add_artist_modal.monitor_new_items_list.current_selection(),
NewItemMonitorType::default()
);
assert_str_eq!(
add_artist_modal.quality_profile_list.current_selection(),
"Standard"
);
assert_str_eq!(
add_artist_modal.metadata_profile_list.current_selection(),
"Standard"
);
assert_eq!(
add_artist_modal.root_folder_list.current_selection(),
&root_folder_1
);
assert_is_empty!(add_artist_modal.tags.text);
}
#[test]
fn test_edit_artist_modal_from_lidarr_data() {