Lidarr support #1

Merged
Dark-Alex-17 merged 61 commits from lidarr into main 2026-01-21 21:30:47 +00:00
28 changed files with 3448 additions and 86 deletions
Showing only changes of commit c624d1b9e4 - Show all commits
+14 -2
View File
@@ -1,8 +1,12 @@
use crate::app::App;
use crate::app::context_clues::{BARE_POPUP_CONTEXT_CLUES, ContextClue, ContextClueProvider};
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS,
};
#[cfg(test)]
#[path = "lidarr_context_clues_tests.rs"]
@@ -47,6 +51,14 @@ impl ContextClueProvider for LidarrContextClueProvider {
_ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveLidarrBlock::AddArtistPrompt
| ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder
| ActiveLidarrBlock::AddArtistTagsInput
| ActiveLidarrBlock::AddArtistAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
_ if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
Some(&ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES)
}
+92 -1
View File
@@ -1,13 +1,14 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{Subcommand, arg};
use clap::{ArgAction, Subcommand, arg};
use tokio::sync::Mutex;
use super::LidarrCommand;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::lidarr_models::{AddArtistBody, AddArtistOptions, MonitorType, NewItemMonitorType},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
@@ -17,6 +18,63 @@ mod add_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrAddCommand {
#[command(about = "Add a new artist to your Lidarr library")]
Artist {
#[arg(
long,
help = "The MusicBrainz foreign artist ID of the artist you wish to add to your library",
required = true
)]
foreign_artist_id: String,
#[arg(long, help = "The name of the artist", required = true)]
artist_name: String,
#[arg(
long,
help = "The root folder path where all artist data and metadata should live",
required = true
)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the quality profile to use for this artist",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the metadata profile to use for this artist",
required = true
)]
metadata_profile_id: i64,
#[arg(long, help = "Disable monitoring for this artist")]
disable_monitoring: bool,
#[arg(
long,
help = "Tag IDs to tag the artist with",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
#[arg(
long,
help = "What Lidarr should monitor for this artist",
value_enum,
default_value_t = MonitorType::default()
)]
monitor: MonitorType,
#[arg(
long,
help = "How Lidarr should monitor new items for this artist",
value_enum,
default_value_t = NewItemMonitorType::default()
)]
monitor_new_items: NewItemMonitorType,
#[arg(
long,
help = "Tell Lidarr to not start a search for missing albums once the artist is added to your library"
)]
no_search_for_missing_albums: bool,
},
#[command(about = "Add new tag")]
Tag {
#[arg(long, help = "The name of the tag to be added", required = true)]
@@ -51,6 +109,39 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHan
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrAddCommand::Artist {
foreign_artist_id,
artist_name,
root_folder_path,
quality_profile_id,
metadata_profile_id,
disable_monitoring,
tag: tags,
monitor,
monitor_new_items,
no_search_for_missing_albums,
} => {
let body = AddArtistBody {
foreign_artist_id,
artist_name,
monitored: !disable_monitoring,
root_folder_path,
quality_profile_id,
metadata_profile_id,
tags,
tag_input_string: None,
add_options: AddArtistOptions {
monitor,
monitor_new_items,
search_for_missing_albums: !no_search_for_missing_albums,
},
};
let resp = self
.network
.handle_network_event(LidarrEvent::AddArtist(body).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrAddCommand::Tag { name } => {
let resp = self
.network
+369 -1
View File
@@ -8,6 +8,7 @@ mod tests {
Command,
lidarr::{LidarrCommand, add_command_handler::LidarrAddCommand},
},
models::lidarr_models::{MonitorType, NewItemMonitorType},
};
use pretty_assertions::assert_eq;
@@ -52,6 +53,321 @@ mod tests {
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_foreign_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_artist_name() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_root_folder_path() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_quality_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_metadata_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_success_with_required_args_only() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
disable_monitoring: false,
tag: vec![],
monitor: MonitorType::default(),
monitor_new_items: NewItemMonitorType::default(),
no_search_for_missing_albums: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_success_with_all_args() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 2,
disable_monitoring: true,
tag: vec![1, 2],
monitor: MonitorType::Future,
monitor_new_items: NewItemMonitorType::New,
no_search_for_missing_albums: true,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--disable-monitoring",
"--tag",
"1",
"--tag",
"2",
"--monitor",
"future",
"--monitor-new-items",
"new",
"--no-search-for-missing-albums",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_monitor_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--monitor",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_artist_new_item_monitor_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--monitor-new-items",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_artist_tags_is_repeatable() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 2,
disable_monitoring: false,
tag: vec![1, 2],
monitor: MonitorType::default(),
monitor_new_items: NewItemMonitorType::default(),
no_search_for_missing_albums: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
}
mod handler {
@@ -64,7 +380,9 @@ mod tests {
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::lidarr_models::{
AddArtistBody, AddArtistOptions, LidarrSerdeable, MonitorType, NewItemMonitorType,
};
use crate::network::lidarr_network::LidarrEvent;
use crate::{
app::App,
@@ -97,5 +415,55 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_add_artist_command() {
let expected_body = AddArtistBody {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
monitored: false,
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddArtistOptions {
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
search_for_missing_albums: false,
},
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddArtist(expected_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_artist_command = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
disable_monitoring: true,
tag: vec![1, 2],
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
no_search_for_missing_albums: true,
};
let result = LidarrAddCommandHandler::with(&app_arc, add_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -1,8 +1,13 @@
use crate::handlers::KeyEventHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock};
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions, AddArtistSearchResult};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ActiveLidarrBlock,
};
use crate::models::servarr_data::lidarr::modals::AddArtistModal;
use crate::models::{BlockSelectionState, Route, Scrollable};
use crate::network::lidarr_network::LidarrEvent;
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)]
#[path = "add_artist_handler_tests.rs"]
@@ -15,6 +20,84 @@ pub struct AddArtistHandler<'a, 'b> {
_context: Option<ActiveLidarrBlock>,
}
impl AddArtistHandler<'_, '_> {
fn build_add_artist_body(&mut self) -> AddArtistBody {
let add_artist_modal = self
.app
.data
.lidarr_data
.add_artist_modal
.take()
.expect("AddArtistModal is None");
let tags = add_artist_modal.tags.text;
let AddArtistModal {
root_folder_list,
monitor_list,
monitor_new_items_list,
quality_profile_list,
metadata_profile_list,
..
} = add_artist_modal;
let (foreign_artist_id, artist_name) = {
let AddArtistSearchResult {
foreign_artist_id,
artist_name,
..
} = self
.app
.data
.lidarr_data
.add_searched_artists
.as_ref()
.unwrap()
.current_selection();
(foreign_artist_id.clone(), artist_name.text.clone())
};
let quality_profile = quality_profile_list.current_selection();
let quality_profile_id = *self
.app
.data
.lidarr_data
.quality_profile_map
.iter()
.filter(|(_, value)| *value == quality_profile)
.map(|(key, _)| key)
.next()
.unwrap();
let metadata_profile = metadata_profile_list.current_selection();
let metadata_profile_id = *self
.app
.data
.lidarr_data
.metadata_profile_map
.iter()
.filter(|(_, value)| *value == metadata_profile)
.map(|(key, _)| key)
.next()
.unwrap();
let path = root_folder_list.current_selection().path.clone();
let monitor = *monitor_list.current_selection();
let monitor_new_items = *monitor_new_items_list.current_selection();
AddArtistBody {
foreign_artist_id,
artist_name,
monitored: true,
root_folder_path: path,
quality_profile_id,
metadata_profile_id,
tags: Vec::new(),
tag_input_string: Some(tags),
add_options: AddArtistOptions {
monitor,
monitor_new_items,
search_for_missing_albums: true,
},
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a, 'b> {
fn handle(&mut self) {
let add_artist_table_handling_config =
@@ -66,74 +149,382 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a,
!self.app.is_loading
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_up(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_up(),
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_up(),
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_up(),
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_up(),
ActiveLidarrBlock::AddArtistSelectRootFolder => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.root_folder_list
.scroll_up(),
ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {}
fn handle_scroll_down(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_down(),
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_down(),
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_down(),
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_down(),
ActiveLidarrBlock::AddArtistSelectRootFolder => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.root_folder_list
.scroll_down(),
ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.down(),
_ => (),
}
}
fn handle_home(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput {
self
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSelectRootFolder => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.root_folder_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSearchInput => self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
.scroll_home();
.scroll_home(),
ActiveLidarrBlock::AddArtistTagsInput => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.tags
.scroll_home(),
_ => (),
}
}
fn handle_end(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput {
self
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSelectRootFolder => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.root_folder_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSearchInput => self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
.reset_offset();
.reset_offset(),
ActiveLidarrBlock::AddArtistTagsInput => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.tags
.reset_offset(),
_ => (),
}
}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
)
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistPrompt => handle_prompt_toggle(self.app, self.key),
ActiveLidarrBlock::AddArtistSearchInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
)
}
ActiveLidarrBlock::AddArtistTagsInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.tags
)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSearchInput => {
let search_text = &self
ActiveLidarrBlock::AddArtistSearchInput
if !self
.app
.data
.lidarr_data
.add_artist_search
.as_ref()
.unwrap()
.text;
.text
.is_empty() =>
{
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveLidarrBlock::AddArtistSearchResults
if self.app.data.lidarr_data.add_searched_artists.is_some() =>
{
let foreign_artist_id = self
.app
.data
.lidarr_data
.add_searched_artists
.as_ref()
.unwrap()
.current_selection()
.foreign_artist_id
.clone();
if !search_text.is_empty() {
if self
.app
.data
.lidarr_data
.artists
.items
.iter()
.any(|artist| artist.foreign_artist_id == foreign_artist_id)
{
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
self.app.ignore_special_keys_for_textbox_input = false;
.push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into());
} else {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into());
self.app.data.lidarr_data.add_artist_modal = Some((&self.app.data.lidarr_data).into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS);
}
}
ActiveLidarrBlock::AddArtistSearchResults => {}
ActiveLidarrBlock::AddArtistPrompt => {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::AddArtistConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::AddArtist(self.build_add_artist_body()));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.push_navigation_stack(
self
.app
.data
.lidarr_data
.selected_block
.get_active_block()
.into(),
),
ActiveLidarrBlock::AddArtistTagsInput => {
self.app.push_navigation_stack(
self
.app
.data
.lidarr_data
.selected_block
.get_active_block()
.into(),
);
self.app.ignore_special_keys_for_textbox_input = true;
}
_ => (),
}
}
ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(),
ActiveLidarrBlock::AddArtistTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
}
@@ -151,23 +542,67 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a,
self.app.data.lidarr_data.add_searched_artists = None;
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveLidarrBlock::AddArtistPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.add_artist_modal = None;
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistAlreadyInLibrary
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(),
ActiveLidarrBlock::AddArtistTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
)
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSearchInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
)
}
ActiveLidarrBlock::AddArtistTagsInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.tags
)
}
ActiveLidarrBlock::AddArtistPrompt => {
if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::AddArtistConfirmPrompt
&& matches_key!(confirm, key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::AddArtist(self.build_add_artist_body()));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
File diff suppressed because it is too large Load Diff
@@ -426,8 +426,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
fn handle_submit(&mut self) {
match self.active_sonarr_block {
_ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchInput
&& !self
ActiveSonarrBlock::AddSeriesSearchInput
if !self
.app
.data
.sonarr_data
@@ -442,8 +442,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into());
self.app.ignore_special_keys_for_textbox_input = false;
}
_ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchResults
&& self.app.data.sonarr_data.add_searched_series.is_some() =>
ActiveSonarrBlock::AddSeriesSearchResults
if self.app.data.sonarr_data.add_searched_series.is_some() =>
{
let tvdb_id = self
.app
@@ -1030,8 +1030,6 @@ mod tests {
app.is_loading = true;
app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into());
let mut add_searched_series = StatefulTable::default();
add_searched_series.set_items(vec![AddSeriesSearchResult::default()]);
AddSeriesHandler::new(
SUBMIT_KEY,
@@ -1053,6 +1051,7 @@ mod tests {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into());
AddSeriesHandler::new(
SUBMIT_KEY,
&mut app,
@@ -1092,7 +1091,7 @@ mod tests {
}
#[test]
fn test_add_series_prompt_prompt_decline_submit() {
fn test_add_series_confirm_prompt_prompt_decline_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into());
@@ -1195,9 +1194,9 @@ mod tests {
.handle();
assert_navigation_popped!(app, ActiveSonarrBlock::Series.into());
assert_eq!(
app.data.sonarr_data.prompt_confirm_action,
Some(SonarrEvent::AddSeries(expected_add_series_body))
assert_some_eq_x!(
&app.data.sonarr_data.prompt_confirm_action,
&SonarrEvent::AddSeries(expected_add_series_body.clone())
);
assert_modal_absent!(app.data.sonarr_data.add_series_modal);
}
+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() {
@@ -1,8 +1,8 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{
AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams, LidarrSerdeable,
NewItemMonitorType,
AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteArtistParams,
EditArtistParams, LidarrSerdeable, MonitorType, NewItemMonitorType,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::network::NetworkResource;
@@ -439,4 +439,105 @@ mod tests {
assert_some!(&app.data.lidarr_data.add_searched_artists);
assert_is_empty!(app.data.lidarr_data.add_searched_artists.as_ref().unwrap());
}
#[tokio::test]
async fn test_handle_add_artist_event() {
let add_artist_body = AddArtistBody {
foreign_artist_id: "test-foreign-id".to_owned(),
artist_name: "Test Artist".to_owned(),
monitored: true,
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
tags: Vec::default(),
tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddArtistOptions {
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
search_for_missing_albums: true,
},
};
let expected_body = json!({
"foreignArtistId": "test-foreign-id",
"artistName": "Test Artist",
"monitored": true,
"rootFolderPath": "/music",
"qualityProfileId": 1,
"metadataProfileId": 1,
"tags": [1, 2],
"addOptions": {
"monitor": "all",
"monitorNewItems": "all",
"searchForMissingAlbums": true
}
});
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(expected_body)
.returns(json!({"id": 1}))
.build_for(LidarrEvent::AddArtist(AddArtistBody::default()))
.await;
app.lock().await.data.lidarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::AddArtist(add_artist_body))
.await
.is_ok()
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_add_artist_event_does_not_overwrite_tags_vec_when_tag_input_string_is_none()
{
let add_artist_body = AddArtistBody {
foreign_artist_id: "test-foreign-id".to_owned(),
artist_name: "Test Artist".to_owned(),
monitored: true,
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddArtistOptions {
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
search_for_missing_albums: true,
},
};
let expected_body = json!({
"foreignArtistId": "test-foreign-id",
"artistName": "Test Artist",
"monitored": true,
"rootFolderPath": "/music",
"qualityProfileId": 1,
"metadataProfileId": 1,
"tags": [1, 2],
"addOptions": {
"monitor": "all",
"monitorNewItems": "all",
"searchForMissingAlbums": true
}
});
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(expected_body)
.returns(json!({"id": 1}))
.build_for(LidarrEvent::AddArtist(add_artist_body.clone()))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::AddArtist(add_artist_body))
.await
.is_ok()
);
mock.assert_async().await;
}
}
+27 -1
View File
@@ -4,7 +4,7 @@ use serde_json::{Value, json};
use crate::models::Route;
use crate::models::lidarr_models::{
AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams,
AddArtistBody, AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_models::CommandBody;
@@ -213,6 +213,32 @@ impl Network<'_, '_> {
result
}
pub(in crate::network::lidarr_network) async fn add_artist(
&mut self,
mut add_artist_body: AddArtistBody,
) -> Result<Value> {
info!("Adding Lidarr artist: {}", add_artist_body.artist_name);
if let Some(tag_input_str) = add_artist_body.tag_input_string.as_ref() {
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
add_artist_body.tags = tag_ids_vec;
}
let event = LidarrEvent::AddArtist(AddArtistBody::default());
let request_props = self
.request_props_from(
event,
RequestMethod::Post,
Some(add_artist_body),
None,
None,
)
.await;
self
.handle_request::<AddArtistBody, Value>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn edit_artist(
&mut self,
mut edit_artist_params: EditArtistParams,
@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::models::lidarr_models::{LidarrSerdeable, MetadataProfile};
use crate::models::lidarr_models::{AddArtistBody, LidarrSerdeable, MetadataProfile};
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
@@ -18,6 +18,7 @@ mod tests {
#[values(
LidarrEvent::GetArtistDetails(0),
LidarrEvent::ListArtists,
LidarrEvent::AddArtist(AddArtistBody::default()),
LidarrEvent::ToggleArtistMonitoring(0)
)]
event: LidarrEvent,
+4 -1
View File
@@ -3,7 +3,7 @@ use log::info;
use super::{NetworkEvent, NetworkResource};
use crate::models::lidarr_models::{
DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
AddArtistBody, DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
};
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
@@ -23,6 +23,7 @@ pub mod lidarr_network_test_utils;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum LidarrEvent {
AddArtist(AddArtistBody),
AddTag(String),
DeleteArtist(DeleteArtistParams),
DeleteTag(i64),
@@ -52,6 +53,7 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::EditArtist(_)
| LidarrEvent::GetArtistDetails(_)
| LidarrEvent::ListArtists
| LidarrEvent::AddArtist(_)
| LidarrEvent::ToggleArtistMonitoring(_) => "/artist",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) => "/queue",
@@ -132,6 +134,7 @@ impl Network<'_, '_> {
.map(LidarrSerdeable::from),
LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from),
LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from),
LidarrEvent::AddArtist(body) => self.add_artist(body).await.map(LidarrSerdeable::from),
}
}
+269 -3
View File
@@ -2,18 +2,22 @@ use std::sync::atomic::Ordering;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::{Cell, Row};
use ratatui::widgets::{Cell, ListItem, Row};
use crate::App;
use crate::models::Route;
use crate::models::lidarr_models::AddArtistSearchResult;
use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock};
use crate::models::servarr_data::lidarr::modals::AddArtistModal;
use crate::render_selectable_input_box;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block, title_block_centered};
use crate::ui::widgets::button::Button;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{DrawUi, draw_popup};
#[cfg(test)]
@@ -31,7 +35,28 @@ impl DrawUi for AddArtistUi {
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
draw_popup(f, app, draw_add_artist_search, Size::Large);
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
draw_popup(f, app, draw_add_artist_search, Size::Large);
match active_lidarr_block {
ActiveLidarrBlock::AddArtistPrompt
| ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder
| ActiveLidarrBlock::AddArtistTagsInput => {
draw_popup(f, app, draw_confirmation_popup, Size::Long);
}
ActiveLidarrBlock::AddArtistAlreadyInLibrary => {
f.render_widget(
Popup::new(Message::new("This artist is already in your library")).size(Size::Message),
f.area(),
);
}
_ => (),
}
}
}
}
@@ -119,7 +144,15 @@ fn draw_add_artist_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(layout_block().default_color(), results_area);
f.render_widget(error_message_popup, f.area());
}
ActiveLidarrBlock::AddArtistSearchResults => {
ActiveLidarrBlock::AddArtistSearchResults
| ActiveLidarrBlock::AddArtistPrompt
| ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder
| ActiveLidarrBlock::AddArtistAlreadyInLibrary
| ActiveLidarrBlock::AddArtistTagsInput => {
let search_results_table = ManagarrTable::new(
app.data.lidarr_data.add_searched_artists.as_mut(),
search_results_row_mapping,
@@ -149,3 +182,236 @@ fn draw_add_artist_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
search_box_area,
);
}
fn draw_confirmation_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
match active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => {
draw_confirmation_prompt(f, app, area);
draw_add_artist_select_monitor_popup(f, app);
}
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => {
draw_confirmation_prompt(f, app, area);
draw_add_artist_select_monitor_new_items_popup(f, app);
}
ActiveLidarrBlock::AddArtistSelectQualityProfile => {
draw_confirmation_prompt(f, app, area);
draw_add_artist_select_quality_profile_popup(f, app);
}
ActiveLidarrBlock::AddArtistSelectMetadataProfile => {
draw_confirmation_prompt(f, app, area);
draw_add_artist_select_metadata_profile_popup(f, app);
}
ActiveLidarrBlock::AddArtistSelectRootFolder => {
draw_confirmation_prompt(f, app, area);
draw_add_artist_select_root_folder_popup(f, app);
}
ActiveLidarrBlock::AddArtistPrompt | ActiveLidarrBlock::AddArtistTagsInput => {
draw_confirmation_prompt(f, app, area)
}
_ => (),
}
}
}
fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let searched_artist = app
.data
.lidarr_data
.add_searched_artists
.as_ref()
.expect("add_searched_artists must be populated")
.current_selection();
let artist_name = &searched_artist.artist_name.text;
let artist_disambiguation = searched_artist.disambiguation.clone().unwrap_or_default();
let title = if artist_disambiguation.is_empty() {
format!("Add - {artist_name}")
} else {
format!("Add - {artist_name} ({artist_disambiguation})")
};
let yes_no_value = app.data.lidarr_data.prompt_confirm;
let selected_block = app.data.lidarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == ActiveLidarrBlock::AddArtistConfirmPrompt;
let AddArtistModal {
monitor_list,
monitor_new_items_list,
quality_profile_list,
metadata_profile_list,
root_folder_list,
tags,
..
} = app
.data
.lidarr_data
.add_artist_modal
.as_ref()
.expect("add_artist_modal must exist in this context");
let selected_monitor = monitor_list.current_selection();
let selected_monitor_new_items = monitor_new_items_list.current_selection();
let selected_quality_profile = quality_profile_list.current_selection();
let selected_metadata_profile = metadata_profile_list.current_selection();
let selected_root_folder = root_folder_list.current_selection();
f.render_widget(title_block_centered(&title), area);
let [
_,
root_folder_area,
monitor_area,
monitor_new_items_area,
quality_profile_area,
metadata_profile_area,
tags_area,
_,
buttons_area,
] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3),
])
.margin(1)
.areas(area);
let [add_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area);
let root_folder_drop_down_button = Button::default()
.title(&selected_root_folder.path)
.label("Root Folder")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddArtistSelectRootFolder);
let monitor_drop_down_button = Button::default()
.title(selected_monitor.to_display_str())
.label("Monitor")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddArtistSelectMonitor);
let monitor_new_items_drop_down_button = Button::default()
.title(selected_monitor_new_items.to_display_str())
.label("Monitor New Items")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddArtistSelectMonitorNewItems);
let quality_profile_drop_down_button = Button::default()
.title(selected_quality_profile)
.label("Quality Profile")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddArtistSelectQualityProfile);
let metadata_profile_drop_down_button = Button::default()
.title(selected_metadata_profile)
.label("Metadata Profile")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddArtistSelectMetadataProfile);
f.render_widget(root_folder_drop_down_button, root_folder_area);
f.render_widget(monitor_drop_down_button, monitor_area);
f.render_widget(monitor_new_items_drop_down_button, monitor_new_items_area);
f.render_widget(quality_profile_drop_down_button, quality_profile_area);
f.render_widget(metadata_profile_drop_down_button, metadata_profile_area);
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let tags_input_box = InputBox::new(&tags.text)
.offset(tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == ActiveLidarrBlock::AddArtistTagsInput)
.selected(active_lidarr_block == ActiveLidarrBlock::AddArtistTagsInput);
render_selectable_input_box!(tags_input_box, f, tags_area);
}
let add_button = Button::default()
.title("Add")
.selected(yes_no_value && highlight_yes_no);
let cancel_button = Button::default()
.title("Cancel")
.selected(!yes_no_value && highlight_yes_no);
f.render_widget(add_button, add_area);
f.render_widget(cancel_button, cancel_area);
}
fn draw_add_artist_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let monitor_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.expect("add_artist_modal must exist in this context")
.monitor_list,
|monitor| ListItem::new(monitor.to_display_str().to_owned()),
);
let popup = Popup::new(monitor_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
fn draw_add_artist_select_monitor_new_items_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let monitor_new_items_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.expect("add_artist_modal must exist in this context")
.monitor_new_items_list,
|monitor_new_items| ListItem::new(monitor_new_items.to_display_str().to_owned()),
);
let popup = Popup::new(monitor_new_items_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
fn draw_add_artist_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let quality_profile_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.expect("add_artist_modal must exist in this context")
.quality_profile_list,
|quality_profile| ListItem::new(quality_profile.clone()),
);
let popup = Popup::new(quality_profile_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
fn draw_add_artist_select_metadata_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let metadata_profile_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.expect("add_artist_modal must exist in this context")
.metadata_profile_list,
|metadata_profile| ListItem::new(metadata_profile.clone()),
);
let popup = Popup::new(metadata_profile_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
fn draw_add_artist_select_root_folder_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let root_folder_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.expect("add_artist_modal must exist in this context")
.root_folder_list,
|root_folder| ListItem::new(root_folder.path.to_owned()),
);
let popup = Popup::new(root_folder_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
@@ -20,7 +20,8 @@ mod tests {
mod snapshot_tests {
use super::*;
use crate::app::App;
use crate::models::HorizontallyScrollableText;
use crate::models::servarr_data::lidarr::lidarr_data::ADD_ARTIST_SELECTION_BLOCKS;
use crate::models::{BlockSelectionState, HorizontallyScrollableText};
use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app};
use rstest::rstest;
@@ -57,5 +58,67 @@ mod tests {
insta::assert_snapshot!(format!("add_artist_ui_{active_lidarr_block}"), output);
}
#[rstest]
#[case(ActiveLidarrBlock::AddArtistPrompt)]
#[case(ActiveLidarrBlock::AddArtistConfirmPrompt)]
#[case(ActiveLidarrBlock::AddArtistSelectMonitor)]
#[case(ActiveLidarrBlock::AddArtistSelectMonitorNewItems)]
#[case(ActiveLidarrBlock::AddArtistSelectQualityProfile)]
#[case(ActiveLidarrBlock::AddArtistSelectMetadataProfile)]
#[case(ActiveLidarrBlock::AddArtistSelectRootFolder)]
#[case(ActiveLidarrBlock::AddArtistTagsInput)]
fn test_add_artist_modal_ui_renders(#[case] active_lidarr_block: ActiveLidarrBlock) {
use crate::models::lidarr_models::{MonitorType, NewItemMonitorType};
use crate::models::servarr_data::lidarr::modals::AddArtistModal;
use crate::models::servarr_models::RootFolder;
use strum::IntoEnumIterator;
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS);
let mut add_artist_modal = AddArtistModal {
tags: "test".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
.quality_profile_list
.set_items(vec!["Any".to_owned()]);
add_artist_modal
.metadata_profile_list
.set_items(vec!["Standard".to_owned()]);
add_artist_modal
.root_folder_list
.set_items(vec![RootFolder {
path: "/nfs/music".to_owned(),
..RootFolder::default()
}]);
app.data.lidarr_data.add_artist_modal = Some(add_artist_modal);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AddArtistUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("add_artist_modal_{active_lidarr_block}"), output);
}
#[test]
fn test_add_artist_already_in_library_ui_renders() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AddArtistUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
@@ -0,0 +1,47 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮
│Test Artist │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✔ Name Type Status Rating Genres │
│=> Test Artist Person Continuing 8.4 soundtrack │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────── Error ───────────────╮ │
│ │This artist is already in your library │ │
│ │ │ │
│ ╰───────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,14 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮
│Test Artist │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮
╭──────│ │───────╮
│Test A│ │ │
╰──────│ │───────╯
╭──────│ │───────╮
│ ✔ │ │ │
│=> │ │ │
│ │ │ │
│ │ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Root Folder: │/nfs/music ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Monitor: │All Albums ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Monitor New Items: │All Albums ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Quality Profile: │Any ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Metadata Profile: │Standard ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Tags: │test │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │
│ ││ Add ││ Cancel ││ │
╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮
╭──────│ │───────╮
│Test A│ │ │
╰──────│ │───────╯
╭──────│ │───────╮
│ ✔ │ │ │
│=> │ │ │
│ │ │ │
│ │ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Root Folder: │/nfs/music ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │
│ │ │Standard │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Monito│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Qual│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Metad│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ │ │ │ │ │
│ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │
│ ││ Add ││ Cancel ││ │
╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮
╭──────│ │───────╮
│Test A│ │ │
╰──────│ │───────╯
╭──────│ │───────╮
│ ✔ │ │ │
│=> │ │ │
│ │ │ │
│ │ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Root Folder: │/nfs/music ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │
│ │ │All Albums │ ▼ │ │ │
│ │ │Future Albums │──────────────────────────────╯ │ │
│ │ │Missing Albums │──────────────────────────────╮ │ │
│ │ Monito│Existing Albums │ ▼ │ │ │
│ │ │First Album │──────────────────────────────╯ │ │
│ │ │Latest Album │──────────────────────────────╮ │ │
│ │ Qual│None │ ▼ │ │ │
│ │ │Unknown │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Metad│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ │ │ │ │ │
│ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │
│ ││ Add ││ Cancel ││ │
╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮
╭──────│ │───────╮
│Test A│ │ │
╰──────│ │───────╯
╭──────│ │───────╮
│ ✔ │ │ │
│=> │ │ │
│ │ │ │
│ │ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Root Folder: │/nfs/music ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │
│ │ │All Albums │ ▼ │ │ │
│ │ │No New Albums │──────────────────────────────╯ │ │
│ │ │New Albums │──────────────────────────────╮ │ │
│ │ Monito│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Qual│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Metad│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ │ │ │ │ │
│ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │
│ ││ Add ││ Cancel ││ │
╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮
╭──────│ │───────╮
│Test A│ │ │
╰──────│ │───────╯
╭──────│ │───────╮
│ ✔ │ │ │
│=> │ │ │
│ │ │ │
│ │ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Root Folder: │/nfs/music ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │
│ │ │Any │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Monito│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Qual│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Metad│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ │ │ │ │ │
│ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │
│ ││ Add ││ Cancel ││ │
╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮
╭──────│ │───────╮
│Test A│ │ │
╰──────│ │───────╯
╭──────│ │───────╮
│ ✔ │ │ │
│=> │ │ │
│ │ │ │
│ │ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Root Folder: │/nfs/music ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │
│ │ │/nfs/music │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Monito│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Qual│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ Metad│ │ ▼ │ │ │
│ │ │ │──────────────────────────────╯ │ │
│ │ │ │──────────────────────────────╮ │ │
│ │ │ │ │ │ │
│ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │
│ ││ Add ││ Cancel ││ │
╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮
╭──────│ │───────╮
│Test A│ │ │
╰──────│ │───────╯
╭──────│ │───────╮
│ ✔ │ │ │
│=> │ │ │
│ │ │ │
│ │ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Root Folder: │/nfs/music ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Monitor: │All Albums ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Monitor New Items: │All Albums ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Quality Profile: │Any ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Metadata Profile: │Standard ▼ │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ ╭─────────────────────────────────────────────────╮ │ │
│ │ Tags: │test │ │ │
│ │ ╰─────────────────────────────────────────────────╯ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │
│ ││ Add ││ Cancel ││ │
╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯