Compare commits

...

10 Commits

111 changed files with 8140 additions and 705 deletions
+39 -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, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS,
};
#[cfg(test)]
#[path = "lidarr_context_clues_tests.rs"]
@@ -32,6 +36,26 @@ pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.delete, "delete album"),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
"toggle album monitoring",
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub(in crate::app) struct LidarrContextClueProvider;
impl ContextClueProvider for LidarrContextClueProvider {
@@ -41,12 +65,25 @@ impl ContextClueProvider for LidarrContextClueProvider {
};
match active_lidarr_block {
_ if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
.data
.lidarr_data
.artist_info_tabs
.get_active_route_contextual_help(),
ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults => {
Some(&BARE_POPUP_CONTEXT_CLUES)
}
_ 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)
}
+93 -3
View File
@@ -2,13 +2,16 @@
mod tests {
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider,
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::lidarr::lidarr_context_clues::{
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
LidarrContextClueProvider,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, LidarrData,
};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use rstest::rstest;
@@ -65,6 +68,54 @@ mod tests {
assert_none!(artists_context_clues_iter.next());
}
#[test]
fn test_artist_details_context_clues() {
let mut artist_details_context_clues_iter = ARTIST_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete album")
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
"toggle album monitoring",
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(artist_details_context_clues_iter.next());
}
#[test]
fn test_add_artist_search_results_context_clues() {
let mut add_artist_search_results_context_clues_iter =
@@ -92,6 +143,23 @@ mod tests {
LidarrContextClueProvider::get_context_clues(&mut app);
}
#[rstest]
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_artist_info_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.lidarr_data = LidarrData::default();
app.data.lidarr_data.artist_info_tabs.set_index(index);
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[test]
fn test_lidarr_context_clue_provider_artists_block() {
let mut app = App::test_default();
@@ -169,4 +237,26 @@ mod tests {
assert_some_eq_x!(context_clues, &ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES);
}
#[rstest]
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_add_artist_blocks(
#[values(
ActiveLidarrBlock::AddArtistPrompt,
ActiveLidarrBlock::AddArtistSelectMonitor,
ActiveLidarrBlock::AddArtistSelectMonitorNewItems,
ActiveLidarrBlock::AddArtistSelectQualityProfile,
ActiveLidarrBlock::AddArtistSelectMetadataProfile,
ActiveLidarrBlock::AddArtistSelectRootFolder,
ActiveLidarrBlock::AddArtistTagsInput,
ActiveLidarrBlock::AddArtistAlreadyInLibrary
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
}
+26
View File
@@ -4,6 +4,7 @@ mod tests {
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::network::NetworkEvent;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
use pretty_assertions::{assert_eq, assert_str_eq};
use tokio::sync::mpsc;
@@ -32,6 +33,23 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_lidarr_block_artist_details() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.artists.set_items(vec![artist()]);
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistDetails)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into());
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
@@ -60,4 +78,12 @@ mod tests {
assert_str_eq!(query, "Test Artist");
}
#[tokio::test]
async fn test_extract_artist_id() {
let mut app = App::test_default();
app.data.lidarr_data.artists.set_items(vec![artist()]);
assert_eq!(app.extract_artist_id().await, 1);
}
}
+9
View File
@@ -28,6 +28,11 @@ impl App<'_> {
.dispatch_network_event(LidarrEvent::ListArtists.into())
.await;
}
ActiveLidarrBlock::ArtistDetails => {
self
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
.await;
}
ActiveLidarrBlock::AddArtistSearchResults => {
self
.dispatch_network_event(
@@ -53,6 +58,10 @@ impl App<'_> {
.clone()
}
async fn extract_artist_id(&self) -> i64 {
self.data.lidarr_data.artists.current_selection().id
}
async fn check_for_lidarr_prompt_action(&mut self) {
if self.data.lidarr_data.prompt_confirm {
self.data.lidarr_data.prompt_confirm = false;
+4 -3
View File
@@ -6,7 +6,8 @@ mod tests {
use crate::app::App;
use crate::app::radarr::ActiveRadarrBlock;
use crate::models::radarr_models::{
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease,
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, MinimumAvailability, Movie,
MovieMonitor, RadarrRelease,
};
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_models::Indexer;
@@ -88,13 +89,13 @@ mod tests {
tmdb_id: 1234,
title: "Test".to_owned(),
root_folder_path: "/nfs2".to_owned(),
minimum_availability: "announced".to_owned(),
minimum_availability: MinimumAvailability::Announced,
monitored: true,
quality_profile_id: 2222,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddMovieOptions {
monitor: "movieOnly".to_owned(),
monitor: MovieMonitor::MovieOnly,
search_for_movie: true,
},
};
+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);
}
}
}
+27 -2
View File
@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::lidarr_models::DeleteArtistParams,
models::lidarr_models::DeleteParams,
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
@@ -19,6 +19,15 @@ mod delete_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrDeleteCommand {
#[command(about = "Delete an album from your Lidarr library")]
Album {
#[arg(long, help = "The ID of the album to delete", required = true)]
album_id: i64,
#[arg(long, help = "Delete the album files from disk as well")]
delete_files_from_disk: bool,
#[arg(long, help = "Add a list exclusion for this album")]
add_list_exclusion: bool,
},
#[command(about = "Delete an artist from your Lidarr library")]
Artist {
#[arg(long, help = "The ID of the artist to delete", required = true)]
@@ -62,12 +71,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrDeleteCommand::Album {
album_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_album_params = DeleteParams {
id: album_id,
delete_files: delete_files_from_disk,
add_import_list_exclusion: add_list_exclusion,
};
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteAlbum(delete_album_params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Artist {
artist_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_artist_params = DeleteArtistParams {
let delete_artist_params = DeleteParams {
id: artist_id,
delete_files: delete_files_from_disk,
add_import_list_exclusion: add_list_exclusion,
+95 -2
View File
@@ -27,6 +27,65 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_delete_album_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "album"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_album_defaults() {
let expected_args = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result =
Cli::try_parse_from(["managarr", "lidarr", "delete", "album", "--album-id", "1"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_album_all_args_defined() {
let expected_args = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"album",
"--album-id",
"1",
"--delete-files-from-disk",
"--add-list-exclusion",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]);
@@ -128,14 +187,48 @@ mod tests {
},
models::{
Serdeable,
lidarr_models::{DeleteArtistParams, LidarrSerdeable},
lidarr_models::{DeleteParams, LidarrSerdeable},
},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_delete_album_command() {
let expected_delete_album_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteAlbum(expected_delete_album_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_album_command = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_album_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_artist_command() {
let expected_delete_artist_params = DeleteArtistParams {
let expected_delete_artist_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
+16
View File
@@ -18,6 +18,15 @@ mod get_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrGetCommand {
#[command(about = "Get detailed information for the album with the given ID")]
AlbumDetails {
#[arg(
long,
help = "The Lidarr ID of the album whose details you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "Get detailed information for the artist with the given ID")]
ArtistDetails {
#[arg(
@@ -62,6 +71,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHan
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrGetCommand::AlbumDetails { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbumDetails(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::ArtistDetails { artist_id } => {
let resp = self
.network
@@ -23,6 +23,32 @@ mod tests {
use super::*;
#[test]
fn test_album_details_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "album-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"get",
"album-details",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_artist_details_requires_artist_id() {
let result =
@@ -91,6 +117,32 @@ mod tests {
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_get_album_details_command() {
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumDetails(expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_album_details_command = LidarrGetCommand::AlbumDetails { album_id: 1 };
let result =
LidarrGetCommandHandler::with(&app_arc, get_album_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_artist_details_command() {
let expected_artist_id = 1;
+60 -2
View File
@@ -75,6 +75,31 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_toggle_album_monitoring_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-album-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_album_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"toggle-album-monitoring",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_search_new_artist_requires_query() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "search-new-artist"]);
@@ -110,6 +135,7 @@ mod tests {
use crate::cli::lidarr::add_command_handler::LidarrAddCommand;
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
use crate::{
app::App,
cli::{
@@ -121,7 +147,7 @@ mod tests {
},
models::{
Serdeable,
lidarr_models::{Artist, DeleteArtistParams, LidarrSerdeable},
lidarr_models::{Artist, DeleteParams, LidarrSerdeable},
},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
@@ -177,7 +203,7 @@ mod tests {
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
let expected_delete_artist_params = DeleteArtistParams {
let expected_delete_artist_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
@@ -306,5 +332,37 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist {
artist_id: 1,
});
let result = LidarrCliHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
+17 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use clap::{Subcommand, arg};
use tokio::sync::Mutex;
use crate::{
@@ -18,6 +18,15 @@ mod list_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrListCommand {
#[command(about = "List all albums for the artist with the given ID")]
Albums {
#[arg(
long,
help = "The Lidarr ID of the artist whose albums you want to list",
required = true
)]
artist_id: i64,
},
#[command(about = "List all artists in your Lidarr library")]
Artists,
#[command(about = "List all Lidarr metadata profiles")]
@@ -55,6 +64,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrListCommand::Albums { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbums(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Artists => {
let resp = self
.network
@@ -19,6 +19,8 @@ mod tests {
mod cli {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
@@ -29,6 +31,32 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_list_albums_requires_artist_id() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "albums"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_albums_with_artist_id() {
let expected_args = LidarrListCommand::Albums { artist_id: 1 };
let result =
Cli::try_parse_from(["managarr", "lidarr", "list", "albums", "--artist-id", "1"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(album_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(album_command, expected_args);
}
}
mod handler {
@@ -77,5 +105,27 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_albums_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetAlbums(1).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_command = LidarrListCommand::Albums { artist_id: 1 };
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
+36
View File
@@ -9,6 +9,9 @@ use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
use list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
use tokio::sync::Mutex;
use trigger_automatic_search_command_handler::{
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
};
use crate::network::lidarr_network::LidarrEvent;
use crate::{app::App, network::NetworkTrait};
@@ -21,6 +24,7 @@ mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod refresh_command_handler;
mod trigger_automatic_search_command_handler;
#[cfg(test)]
#[path = "lidarr_command_tests.rs"]
@@ -58,6 +62,11 @@ pub enum LidarrCommand {
about = "Commands to refresh the data in your Lidarr instance"
)]
Refresh(LidarrRefreshCommand),
#[command(
subcommand,
about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance"
)]
TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand),
#[command(about = "Search for a new artist to add to Lidarr")]
SearchNewArtist {
#[arg(
@@ -67,6 +76,17 @@ pub enum LidarrCommand {
)]
query: String,
},
#[command(
about = "Toggle monitoring for the specified album corresponding to the given album ID"
)]
ToggleAlbumMonitoring {
#[arg(
long,
help = "The Lidarr ID of the album to toggle monitoring on",
required = true
)]
album_id: i64,
},
#[command(
about = "Toggle monitoring for the specified artist corresponding to the given artist ID"
)]
@@ -137,6 +157,15 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
.handle()
.await?
}
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
LidarrTriggerAutomaticSearchCommandHandler::with(
self.app,
trigger_automatic_search_command,
self.network,
)
.handle()
.await?
}
LidarrCommand::SearchNewArtist { query } => {
let resp = self
.network
@@ -144,6 +173,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::ToggleAlbumMonitoring { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::ToggleAlbumMonitoring(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::ToggleArtistMonitoring { artist_id } => {
let resp = self
.network
+16
View File
@@ -19,6 +19,15 @@ mod refresh_command_handler_tests;
pub enum LidarrRefreshCommand {
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
AllArtists,
#[command(about = "Refresh artist data and scan disk for the artist with the given ID")]
Artist {
#[arg(
long,
help = "The ID of the artist to refresh information on and to scan the disk for",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrRefreshCommand> for Command {
@@ -57,6 +66,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrRefreshCommand::Artist { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateAndScanArtist(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
@@ -20,6 +20,8 @@ mod tests {
mod cli {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
#[test]
fn test_refresh_all_artists_has_no_arg_requirements() {
@@ -28,6 +30,37 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_refresh_artist_requires_artist_id() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_refresh_artist_with_artist_id() {
let expected_args = LidarrRefreshCommand::Artist { artist_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"refresh",
"artist",
"--artist-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Refresh(refresh_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(refresh_command, expected_args);
}
}
mod handler {
@@ -68,5 +101,29 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_refresh_artist_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::UpdateAndScanArtist(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = LidarrRefreshCommand::Artist { artist_id: 1 };
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -0,0 +1,72 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "trigger_automatic_search_command_handler_tests.rs"]
mod trigger_automatic_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrTriggerAutomaticSearchCommand {
#[command(about = "Trigger an automatic search for the artist with the specified ID")]
Artist {
#[arg(
long,
help = "The ID of the artist you want to trigger an automatic search for",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrTriggerAutomaticSearchCommand> for Command {
fn from(value: LidarrTriggerAutomaticSearchCommand) -> Self {
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(value))
}
}
pub(super) struct LidarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrTriggerAutomaticSearchCommand>
for LidarrTriggerAutomaticSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrTriggerAutomaticSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrTriggerAutomaticSearchCommand::Artist { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::TriggerAutomaticArtistSearch(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,107 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::Cli;
use crate::cli::{
Command,
lidarr::{
LidarrCommand, trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand,
},
};
use clap::CommandFactory;
#[test]
fn test_lidarr_trigger_automatic_search_command_from() {
let command = LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(command))
);
}
mod cli {
use super::*;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_trigger_automatic_artist_search_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"artist",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_artist_search_with_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"artist",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::lidarr::trigger_automatic_search_command_handler::{
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
};
use crate::{app::App, cli::CliCommandHandler};
use crate::{
models::{Serdeable, lidarr_models::LidarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_trigger_automatic_artist_search_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
+2 -2
View File
@@ -122,12 +122,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
title: String::new(),
root_folder_path,
quality_profile_id,
minimum_availability: minimum_availability.to_string(),
minimum_availability,
monitored: !disable_monitoring,
tags,
tag_input_string: None,
add_options: AddMovieOptions {
monitor: monitor.to_string(),
monitor,
search_for_movie: !no_search_for_movie,
},
};
+2 -2
View File
@@ -384,12 +384,12 @@ mod tests {
title: String::new(),
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
minimum_availability: "released".to_owned(),
minimum_availability: MinimumAvailability::Released,
monitored: false,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddMovieOptions {
monitor: "movieAndCollection".to_owned(),
monitor: MovieMonitor::MovieAndCollection,
search_for_movie: false,
},
};
+2 -2
View File
@@ -137,12 +137,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan
root_folder_path,
quality_profile_id,
language_profile_id,
series_type: series_type.to_string(),
series_type,
season_folder: !disable_season_folders,
tags,
tag_input_string: None,
add_options: AddSeriesOptions {
monitor: monitor.to_string(),
monitor,
search_for_cutoff_unmet_episodes: !no_search_for_series,
search_for_missing_episodes: !no_search_for_series,
},
+2 -2
View File
@@ -517,13 +517,13 @@ mod tests {
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
language_profile_id: 1,
series_type: "anime".to_owned(),
series_type: SeriesType::Anime,
monitored: false,
tags: vec![1, 2],
tag_input_string: None,
season_folder: false,
add_options: AddSeriesOptions {
monitor: "future".to_owned(),
monitor: SeriesMonitor::Future,
search_for_cutoff_unmet_episodes: false,
search_for_missing_episodes: false,
},
@@ -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
@@ -0,0 +1,227 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
use crate::models::lidarr_models::Album;
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS,
EDIT_ARTIST_SELECTION_BLOCKS,
};
use crate::models::{BlockSelectionState, Route};
use crate::network::lidarr_network::LidarrEvent;
#[cfg(test)]
#[path = "artist_details_handler_tests.rs"]
mod artist_details_handler_tests;
pub struct ArtistDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl ArtistDetailsHandler<'_, '_> {
fn extract_artist_id(&self) -> i64 {
self.app.data.lidarr_data.artists.current_selection().id
}
fn extract_album_id(&self) -> i64 {
self.app.data.lidarr_data.albums.current_selection().id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler<'a, 'b> {
fn handle(&mut self) {
let albums_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::ArtistDetails.into())
.searching_block(ActiveLidarrBlock::SearchAlbums.into())
.search_error_block(ActiveLidarrBlock::SearchAlbumsError.into())
.search_field_fn(|album: &Album| &album.title.text);
if !handle_table(
self,
|app| &mut app.data.lidarr_data.albums,
albums_table_handling_config,
) {
match self.active_lidarr_block {
_ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => {
DeleteAlbumHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ => self.handle_key_event(),
};
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
DeleteAlbumHandler::accepts(active_block) || ARTIST_DETAILS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> ArtistDetailsHandler<'a, 'b> {
ArtistDetailsHandler {
key,
app,
active_lidarr_block: active_block,
context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::ArtistDetails {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::UpdateAndScanArtistPrompt
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
handle_prompt_toggle(self.app, self.key);
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(
LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()),
);
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id()));
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::UpdateAndScanArtistPrompt
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::ArtistDetails => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_artist_info_tabs();
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistDetails => match self.key {
_ if matches_key!(refresh, key) => self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into()),
_ if matches_key!(auto_search, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into());
}
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
}
_ if matches_key!(edit, key) => {
self.app.push_navigation_stack(
(
ActiveLidarrBlock::EditArtistPrompt,
Some(self.active_lidarr_block),
)
.into(),
);
self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
}
_ if matches_key!(toggle_monitoring, key) => {
if !self.app.data.lidarr_data.albums.is_empty() {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::ToggleAlbumMonitoring(self.extract_album_id()));
self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
}
_ => (),
},
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(
LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()),
);
self.app.pop_navigation_stack();
}
}
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id()));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,565 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
};
mod test_handle_delete {
use super::*;
use crate::assert_delete_prompt;
use crate::event::Key;
use crate::models::lidarr_models::Album;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS;
use pretty_assertions::assert_eq;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
#[test]
fn test_album_delete() {
let mut app = App::test_default();
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
assert_delete_prompt!(
ArtistDetailsHandler,
app,
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::DeleteAlbumPrompt
);
assert_eq!(
app.data.lidarr_data.selected_block.blocks,
DELETE_ALBUM_SELECTION_BLOCKS
);
}
}
mod test_handle_left_right_action {
use rstest::rstest;
use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
#[rstest]
fn test_left_right_prompt_toggle(
#[values(
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
ActiveLidarrBlock::AutomaticallySearchArtistPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
#[values(Key::Left, Key::Right)] key: Key,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
ArtistDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_popped;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
use rstest::rstest;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[rstest]
#[case(
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
LidarrEvent::TriggerAutomaticArtistSearch(1)
)]
#[case(
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
LidarrEvent::UpdateAndScanArtist(1)
)]
fn test_artist_details_prompt_confirm_submit(
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
) {
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.artists.set_items(vec![artist()]);
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(prompt_block.into());
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&expected_action
);
}
#[rstest]
fn test_artist_details_prompt_decline_submit(
#[values(
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::UpdateAndScanArtistPrompt
)]
prompt_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(prompt_block.into());
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_none!(app.data.lidarr_data.prompt_confirm_action);
}
}
mod test_handle_esc {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_popped;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use rstest::rstest;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
fn test_artist_details_esc(
#[values(
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::UpdateAndScanArtistPrompt
)]
prompt_block: ActiveLidarrBlock,
#[values(true, false)] is_ready: bool,
) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(prompt_block.into());
ArtistDetailsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
}
}
mod test_handle_char_key_event {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::lidarr_models::Artist;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS,
};
use crate::network::lidarr_network::LidarrEvent;
use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_artist_details_edit_key(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.edit.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_navigation_pushed!(
app,
(
ActiveLidarrBlock::EditArtistPrompt,
Some(ActiveLidarrBlock::ArtistDetails)
)
.into()
);
assert_modal_present!(app.data.lidarr_data.edit_artist_modal);
assert!(app.data.lidarr_data.edit_artist_modal.is_some());
assert_eq!(
app.data.lidarr_data.selected_block.blocks,
EDIT_ARTIST_SELECTION_BLOCKS
);
}
#[rstest]
fn test_artist_details_edit_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.edit.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_lidarr_block.into());
assert_modal_absent!(app.data.lidarr_data.edit_artist_modal);
}
#[test]
fn test_artist_details_toggle_monitoring_key() {
let mut app = App::test_default_fully_populated();
app.is_routing = false;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
&mut app,
ActiveLidarrBlock::ArtistDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ArtistDetails.into()
);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(app.is_routing);
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::ToggleAlbumMonitoring(1))
);
}
#[test]
fn test_artist_details_toggle_monitoring_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.data.lidarr_data.prompt_confirm = false;
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
&mut app,
ActiveLidarrBlock::ArtistDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ArtistDetails.into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
}
#[test]
fn test_artist_details_toggle_monitoring_key_no_op_when_albums_empty() {
let mut app = App::test_default();
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
&mut app,
ActiveLidarrBlock::ArtistDetails,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(app.data.lidarr_data.prompt_confirm_action.is_none());
}
#[rstest]
fn test_artist_details_auto_search_key(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.auto_search.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_navigation_pushed!(
app,
ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into()
);
}
#[rstest]
fn test_artist_details_auto_search_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.auto_search.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_lidarr_block.into());
}
#[rstest]
fn test_artist_details_update_key(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
}
#[rstest]
fn test_artist_details_update_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_lidarr_block.into());
}
#[rstest]
fn test_artist_details_refresh_key(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.is_routing = false;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_navigation_pushed!(app, active_lidarr_block.into());
assert!(app.is_routing);
}
#[rstest]
fn test_artist_details_refresh_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(active_lidarr_block.into());
app.is_routing = false;
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_lidarr_block.into());
assert!(!app.is_routing);
}
#[rstest]
#[case(
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
LidarrEvent::TriggerAutomaticArtistSearch(1)
)]
#[case(
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
LidarrEvent::UpdateAndScanArtist(1)
)]
fn test_artist_details_prompt_confirm_key(
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.push_navigation_stack(prompt_block.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
prompt_block,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_navigation_popped!(app, active_lidarr_block.into());
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&expected_action
);
}
}
#[test]
fn test_artist_details_handler_accepts() {
let mut artist_details_blocks = ARTIST_DETAILS_BLOCKS.clone().to_vec();
artist_details_blocks.extend(DELETE_ALBUM_BLOCKS);
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if artist_details_blocks.contains(&active_lidarr_block) {
assert!(ArtistDetailsHandler::accepts(active_lidarr_block));
} else {
assert!(!ArtistDetailsHandler::accepts(active_lidarr_block));
}
});
}
#[test]
fn test_extract_artist_id() {
let mut app = App::test_default_fully_populated();
let artist_id = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ArtistDetails,
None,
)
.extract_artist_id();
assert_eq!(artist_id, 1);
}
#[test]
fn test_extract_album_id() {
let mut app = App::test_default_fully_populated();
let album_id = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ArtistDetails,
None,
)
.extract_album_id();
assert_eq!(album_id, 1);
}
#[rstest]
fn test_artist_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ArtistDetails,
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_artist_details_handler_is_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.is_loading = true;
let handler = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ArtistDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_artist_details_handler_is_ready_when_not_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.is_loading = false;
let handler = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ArtistDetails,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,150 @@
use crate::models::Route;
use crate::models::lidarr_models::DeleteParams;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_BLOCKS;
use crate::network::lidarr_network::LidarrEvent;
use crate::{
app::App,
event::Key,
handlers::{KeyEventHandler, handle_prompt_toggle},
matches_key,
models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
};
#[cfg(test)]
#[path = "delete_album_handler_tests.rs"]
mod delete_album_handler_tests;
pub(in crate::handlers::lidarr_handlers) struct DeleteAlbumHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl DeleteAlbumHandler<'_, '_> {
fn build_delete_album_params(&mut self) -> DeleteParams {
let id = self.app.data.lidarr_data.albums.current_selection().id;
let delete_files = self.app.data.lidarr_data.delete_files;
let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion;
self.app.data.lidarr_data.reset_delete_preferences();
DeleteParams {
id,
delete_files,
add_import_list_exclusion,
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteAlbumHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
DELETE_ALBUM_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
) -> Self {
DeleteAlbumHandler {
key,
app,
active_lidarr_block: active_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading
}
fn handle_scroll_up(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
self.app.data.lidarr_data.selected_block.up();
}
}
fn handle_scroll_down(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
self.app.data.lidarr_data.selected_block.down();
}
}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
handle_prompt_toggle(self.app, self.key);
}
}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::DeleteAlbumConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteAlbum(self.build_delete_album_params()));
self.app.should_refresh = true;
} else {
self.app.data.lidarr_data.reset_delete_preferences();
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile => {
self.app.data.lidarr_data.delete_files = !self.app.data.lidarr_data.delete_files;
}
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion => {
self.app.data.lidarr_data.add_import_list_exclusion =
!self.app.data.lidarr_data.add_import_list_exclusion;
}
_ => (),
}
}
}
fn handle_esc(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_delete_preferences();
self.app.data.lidarr_data.prompt_confirm = false;
}
}
fn handle_char_key_event(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt
&& self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::DeleteAlbumConfirmPrompt
&& matches_key!(confirm, self.key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteAlbum(self.build_delete_album_params()));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,404 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
use crate::models::lidarr_models::{Album, DeleteParams};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ALBUM_BLOCKS};
mod test_handle_scroll_up_and_down {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS;
use super::*;
#[rstest]
fn test_delete_album_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
let mut app = App::test_default();
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
if key == Key::Up {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile
);
} else {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteAlbumConfirmPrompt
);
}
}
#[rstest]
fn test_delete_album_prompt_scroll_no_op_when_not_ready(
#[values(Key::Up, Key::Down)] key: Key,
) {
let mut app = App::test_default();
app.is_loading = true;
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion
);
}
}
mod test_handle_left_right_action {
use rstest::rstest;
use super::*;
#[rstest]
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use pretty_assertions::assert_eq;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS;
use crate::network::lidarr_network::LidarrEvent;
use super::*;
use crate::assert_navigation_popped;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_delete_album_prompt_prompt_decline_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_album_confirm_prompt_prompt_confirmation_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
let expected_delete_album_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::DeleteAlbum(expected_delete_album_params))
);
assert!(app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_album_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::DeleteAlbumPrompt.into()
);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(app.data.lidarr_data.delete_files);
assert!(app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_album_toggle_delete_files_submit() {
let current_route = ActiveLidarrBlock::DeleteAlbumPrompt.into();
let mut app = App::test_default();
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(app.data.lidarr_data.delete_files, true);
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(app.data.lidarr_data.delete_files, false);
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
use rstest::rstest;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
fn test_delete_album_prompt_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteAlbumHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
}
mod test_handle_key_char {
use crate::{
assert_navigation_popped,
models::{
BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS,
},
network::lidarr_network::LidarrEvent,
};
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_delete_album_confirm_prompt_prompt_confirm() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
let expected_delete_album_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::DeleteAlbum(expected_delete_album_params))
);
assert!(app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
}
#[test]
fn test_delete_album_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) {
assert!(DeleteAlbumHandler::accepts(active_lidarr_block));
} else {
assert!(!DeleteAlbumHandler::accepts(active_lidarr_block));
}
});
}
#[rstest]
fn test_delete_album_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_delete_album_params() {
let mut app = App::test_default();
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
let expected_delete_album_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
let delete_album_params = DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.build_delete_album_params();
assert_eq!(delete_album_params, expected_delete_album_params);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_album_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.is_loading = true;
let handler = DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_delete_album_handler_ready_when_not_loading() {
let mut app = App::test_default();
app.is_loading = false;
let handler = DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
);
assert!(handler.is_ready());
}
}
@@ -1,5 +1,5 @@
use crate::models::Route;
use crate::models::lidarr_models::DeleteArtistParams;
use crate::models::lidarr_models::DeleteParams;
use crate::network::lidarr_network::LidarrEvent;
use crate::{
app::App,
@@ -21,13 +21,13 @@ pub(in crate::handlers::lidarr_handlers) struct DeleteArtistHandler<'a, 'b> {
}
impl DeleteArtistHandler<'_, '_> {
fn build_delete_artist_params(&mut self) -> DeleteArtistParams {
fn build_delete_artist_params(&mut self) -> DeleteParams {
let id = self.app.data.lidarr_data.artists.current_selection().id;
let delete_files = self.app.data.lidarr_data.delete_artist_files;
let delete_files = self.app.data.lidarr_data.delete_files;
let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion;
self.app.data.lidarr_data.reset_delete_artist_preferences();
self.app.data.lidarr_data.reset_delete_preferences();
DeleteArtistParams {
DeleteParams {
id,
delete_files,
add_import_list_exclusion,
@@ -99,14 +99,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler<
Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params()));
self.app.should_refresh = true;
} else {
self.app.data.lidarr_data.reset_delete_artist_preferences();
self.app.data.lidarr_data.reset_delete_preferences();
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::DeleteArtistToggleDeleteFile => {
self.app.data.lidarr_data.delete_artist_files =
!self.app.data.lidarr_data.delete_artist_files;
self.app.data.lidarr_data.delete_files = !self.app.data.lidarr_data.delete_files;
}
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion => {
self.app.data.lidarr_data.add_import_list_exclusion =
@@ -120,7 +119,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler<
fn handle_esc(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_delete_artist_preferences();
self.app.data.lidarr_data.reset_delete_preferences();
self.app.data.lidarr_data.prompt_confirm = false;
}
}
@@ -9,7 +9,7 @@ mod tests {
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::delete_artist_handler::DeleteArtistHandler;
use crate::models::lidarr_models::{Artist, DeleteArtistParams};
use crate::models::lidarr_models::{Artist, DeleteParams};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS};
mod test_handle_scroll_up_and_down {
@@ -106,7 +106,7 @@ mod tests {
.lidarr_data
.selected_block
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
app.data.lidarr_data.delete_artist_files = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteArtistHandler::new(
@@ -120,7 +120,7 @@ mod tests {
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_artist_files);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
@@ -130,14 +130,14 @@ mod tests {
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_artist_files = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
let expected_delete_artist_params = DeleteArtistParams {
let expected_delete_artist_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
@@ -165,7 +165,7 @@ mod tests {
);
assert!(app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_artist_files);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
@@ -176,7 +176,7 @@ mod tests {
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_artist_files = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteArtistHandler::new(
@@ -194,7 +194,7 @@ mod tests {
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(app.data.lidarr_data.delete_artist_files);
assert!(app.data.lidarr_data.delete_files);
assert!(app.data.lidarr_data.add_import_list_exclusion);
}
@@ -215,7 +215,7 @@ mod tests {
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(app.data.lidarr_data.delete_artist_files, true);
assert_eq!(app.data.lidarr_data.delete_files, true);
DeleteArtistHandler::new(
SUBMIT_KEY,
@@ -226,7 +226,7 @@ mod tests {
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(app.data.lidarr_data.delete_artist_files, false);
assert_eq!(app.data.lidarr_data.delete_files, false);
}
}
@@ -244,7 +244,7 @@ mod tests {
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_artist_files = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteArtistHandler::new(
@@ -257,7 +257,7 @@ mod tests {
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_artist_files);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
}
@@ -279,14 +279,14 @@ mod tests {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.delete_artist_files = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
let expected_delete_artist_params = DeleteArtistParams {
let expected_delete_artist_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
@@ -314,7 +314,7 @@ mod tests {
);
assert!(app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_artist_files);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
}
@@ -357,9 +357,9 @@ mod tests {
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.data.lidarr_data.delete_artist_files = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
let expected_delete_artist_params = DeleteArtistParams {
let expected_delete_artist_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
@@ -374,7 +374,7 @@ mod tests {
.build_delete_artist_params();
assert_eq!(delete_artist_params, expected_delete_artist_params);
assert!(!app.data.lidarr_data.delete_artist_files);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
@@ -11,10 +11,10 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options};
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
use crate::models::lidarr_models::{Album, Artist, ArtistStatistics, ArtistStatus};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS,
EDIT_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
};
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::network::lidarr_network::LidarrEvent;
@@ -26,7 +26,9 @@ mod tests {
fn test_library_handler_accepts() {
let mut library_handler_blocks = Vec::new();
library_handler_blocks.extend(LIBRARY_BLOCKS);
library_handler_blocks.extend(ARTIST_DETAILS_BLOCKS);
library_handler_blocks.extend(DELETE_ARTIST_BLOCKS);
library_handler_blocks.extend(DELETE_ALBUM_BLOCKS);
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
library_handler_blocks.extend(ADD_ARTIST_BLOCKS);
@@ -532,6 +534,31 @@ mod tests {
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_delegates_delete_album_blocks_to_delete_album_handler() {
let mut app = App::test_default();
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ArtistDetails.into()
);
}
#[test]
fn test_delegates_delete_artist_blocks_to_delete_artist_handler() {
let mut app = App::test_default();
@@ -587,6 +614,32 @@ mod tests {
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[rstest]
fn test_delegates_artist_details_blocks_to_artist_details_handler(
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::SearchAlbums,
ActiveLidarrBlock::SearchAlbumsError,
ActiveLidarrBlock::UpdateAndScanArtistPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(active_lidarr_block.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_edit_key() {
let mut app = App::test_default();
+20 -4
View File
@@ -19,11 +19,14 @@ use super::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
mod add_artist_handler;
mod artist_details_handler;
mod delete_album_handler;
mod delete_artist_handler;
mod edit_artist_handler;
use crate::models::Route;
pub(in crate::handlers::lidarr_handlers) use add_artist_handler::AddArtistHandler;
pub(in crate::handlers::lidarr_handlers) use artist_details_handler::ArtistDetailsHandler;
pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler;
pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler;
@@ -74,6 +77,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
EditArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if ArtistDetailsHandler::accepts(self.active_lidarr_block) => {
ArtistDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ => self.handle_key_event(),
}
}
@@ -83,6 +90,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
AddArtistHandler::accepts(active_block)
|| DeleteArtistHandler::accepts(active_block)
|| EditArtistHandler::accepts(active_block)
|| ArtistDetailsHandler::accepts(active_block)
|| LIBRARY_BLOCKS.contains(&active_block)
}
@@ -139,12 +147,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::UpdateAllArtistsPrompt {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
match self.active_lidarr_block {
ActiveLidarrBlock::Artists => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
}
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
}
self.app.pop_navigation_stack();
self.app.pop_navigation_stack();
}
_ => (),
}
}
@@ -83,8 +83,8 @@ impl AddMovieHandler<'_, '_> {
.unwrap();
let path = root_folder_list.current_selection().path.clone();
let monitor = monitor_list.current_selection().to_string();
let minimum_availability = minimum_availability_list.current_selection().to_string();
let monitor = *monitor_list.current_selection();
let minimum_availability = *minimum_availability_list.current_selection();
AddMovieBody {
tmdb_id,
@@ -5,7 +5,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
use crate::models::radarr_models::{
AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie,
DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection,
MovieFile, RadarrRelease, Rating, RatingsList,
MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList,
};
use crate::models::servarr_models::{
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
@@ -470,13 +470,13 @@ pub(in crate::handlers::radarr_handlers) mod utils {
tmdb_id: 1234,
title: "Test".to_owned(),
root_folder_path: "/nfs2".to_owned(),
minimum_availability: "announced".to_owned(),
minimum_availability: MinimumAvailability::Announced,
monitored: true,
quality_profile_id: 2222,
tags: Vec::new(),
tag_input_string: Some("usenet, testing".into()),
add_options: AddMovieOptions {
monitor: "movieOnly".to_owned(),
monitor: MovieMonitor::MovieOnly,
search_for_movie: true,
},
}
@@ -74,8 +74,8 @@ impl AddSeriesHandler<'_, '_> {
.unwrap();
let path = root_folder_list.current_selection().path.clone();
let monitor = monitor_list.current_selection().to_string();
let series_type = series_type_list.current_selection().to_string();
let monitor = *monitor_list.current_selection();
let series_type = *series_type_list.current_selection();
AddSeriesBody {
tvdb_id,
@@ -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());
@@ -1169,12 +1168,12 @@ mod tests {
root_folder_path: "/nfs2".to_owned(),
quality_profile_id: 2222,
language_profile_id: 2222,
series_type: "standard".to_owned(),
series_type: SeriesType::Standard,
season_folder: true,
tags: Vec::default(),
tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddSeriesOptions {
monitor: "all".to_owned(),
monitor: SeriesMonitor::All,
search_for_cutoff_unmet_episodes: true,
search_for_missing_episodes: true,
},
@@ -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);
}
@@ -1647,12 +1646,12 @@ mod tests {
root_folder_path: "/nfs2".to_owned(),
quality_profile_id: 2222,
language_profile_id: 2222,
series_type: "standard".to_owned(),
series_type: SeriesType::Standard,
season_folder: true,
tags: Vec::default(),
tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddSeriesOptions {
monitor: "all".to_owned(),
monitor: SeriesMonitor::All,
search_for_cutoff_unmet_episodes: true,
search_for_missing_episodes: true,
},
@@ -1777,12 +1776,12 @@ mod tests {
root_folder_path: "/nfs2".to_owned(),
quality_profile_id: 2222,
language_profile_id: 2222,
series_type: "standard".to_owned(),
series_type: SeriesType::Standard,
season_folder: true,
tags: Vec::default(),
tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddSeriesOptions {
monitor: "all".to_owned(),
monitor: SeriesMonitor::All,
search_for_cutoff_unmet_episodes: true,
search_for_missing_episodes: true,
},
@@ -327,10 +327,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler
}
}
ActiveSonarrBlock::UpdateAndScanSeriesPrompt => {
if self.app.data.sonarr_data.prompt_confirm {
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id()));
}
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id()));
self.app.pop_navigation_stack();
}
@@ -402,7 +402,7 @@ mod tests {
ActiveSonarrBlock::SeriesDetails.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_modal_absent!(app.data.sonarr_data.prompt_confirm_action);
assert_none!(app.data.sonarr_data.prompt_confirm_action);
assert!(!app.is_routing);
}
@@ -555,7 +555,6 @@ mod tests {
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::test_default();
app.data.sonarr_data.prompt_confirm = true;
app.data.sonarr_data.series.set_items(vec![series()]);
app.push_navigation_stack(active_sonarr_block.into());
app.push_navigation_stack(prompt_block.into());
+106 -1
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 {
@@ -211,14 +245,45 @@ pub struct AddArtistSearchResult {
pub ratings: Option<Ratings>,
}
#[derive(Default, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LidarrCommandBody {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub artist_id: Option<i64>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub struct DeleteArtistParams {
pub struct DeleteParams {
pub id: i64,
pub delete_files: bool,
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 {
@@ -234,6 +299,44 @@ pub struct EditArtistParams {
pub clear_tags: bool,
}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Album {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
pub title: HorizontallyScrollableText,
pub foreign_album_id: String,
pub monitored: bool,
#[serde(default)]
pub any_release_ok: bool,
#[serde(deserialize_with = "super::from_i64")]
pub profile_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub duration: i64,
pub album_type: Option<String>,
pub genres: Vec<String>,
pub ratings: Option<Ratings>,
pub release_date: Option<DateTime<Utc>>,
pub statistics: Option<AlbumStatistics>,
}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AlbumStatistics {
#[serde(deserialize_with = "super::from_i64")]
pub track_file_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub track_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub total_track_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub size_on_disk: i64,
#[serde(deserialize_with = "super::from_f64")]
pub percent_of_tracks: f64,
}
impl Eq for AlbumStatistics {}
impl From<LidarrSerdeable> for Serdeable {
fn from(value: LidarrSerdeable) -> Serdeable {
Serdeable::Lidarr(value)
@@ -243,6 +346,8 @@ impl From<LidarrSerdeable> for Serdeable {
serde_enum_from!(
LidarrSerdeable {
AddArtistSearchResults(Vec<AddArtistSearchResult>),
Albums(Vec<Album>),
Album(Album),
Artist(Artist),
Artists(Vec<Artist>),
DiskSpaces(Vec<DiskSpace>),
+65 -17
View File
@@ -5,8 +5,8 @@ mod tests {
use serde_json::json;
use crate::models::lidarr_models::{
AddArtistSearchResult, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
MetadataProfile, NewItemMonitorType, SystemStatus,
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
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!({}));
@@ -377,6 +401,45 @@ mod tests {
assert_eq!(lidarr_serdeable, LidarrSerdeable::Tags(tags));
}
#[test]
fn test_lidarr_serdeable_from_add_artist_search_results() {
let search_results = vec![AddArtistSearchResult {
foreign_artist_id: "test-id".to_owned(),
..AddArtistSearchResult::default()
}];
let lidarr_serdeable: LidarrSerdeable = search_results.clone().into();
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::AddArtistSearchResults(search_results)
);
}
#[test]
fn test_lidarr_serdeable_from_albums() {
let albums = vec![Album {
id: 1,
..Album::default()
}];
let lidarr_serdeable: LidarrSerdeable = albums.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Albums(albums));
}
#[test]
fn test_lidarr_serdeable_from_album() {
let album = Album {
id: 1,
..Album::default()
};
let lidarr_serdeable: LidarrSerdeable = album.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Album(album));
}
#[test]
fn test_artist_status_display() {
assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing");
@@ -477,19 +540,4 @@ mod tests {
assert!(search_result.genres.is_empty());
assert_none!(&search_result.ratings);
}
#[test]
fn test_lidarr_serdeable_from_add_artist_search_results() {
let search_results = vec![AddArtistSearchResult {
foreign_artist_id: "test-id".to_owned(),
..AddArtistSearchResult::default()
}];
let lidarr_serdeable: LidarrSerdeable = search_results.clone().into();
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::AddArtistSearchResults(search_results)
);
}
}
+15 -3
View File
@@ -27,7 +27,7 @@ pub struct AddMovieBody {
pub title: String,
pub root_folder_path: String,
pub quality_profile_id: i64,
pub minimum_availability: String,
pub minimum_availability: MinimumAvailability,
pub monitored: bool,
pub tags: Vec<i64>,
#[serde(skip_serializing, skip_deserializing)]
@@ -55,7 +55,7 @@ pub struct AddMovieSearchResult {
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddMovieOptions {
pub monitor: String,
pub monitor: MovieMonitor,
pub search_for_movie: bool,
}
@@ -268,8 +268,20 @@ pub enum MinimumAvailability {
}
#[derive(
Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum, Display, EnumDisplayStyle,
Serialize,
Deserialize,
Default,
PartialEq,
Eq,
Clone,
Copy,
Debug,
EnumIter,
ValueEnum,
Display,
EnumDisplayStyle,
)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum MovieMonitor {
#[default]
+126 -21
View File
@@ -1,25 +1,28 @@
use serde_json::Number;
use super::modals::EditArtistModal;
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
use super::modals::{AddArtistModal, EditArtistModal};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
lidarr_models::{AddArtistSearchResult, Artist, DownloadRecord},
lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord},
servarr_models::{DiskSpace, RootFolder},
stateful_table::StatefulTable,
};
use crate::network::lidarr_network::LidarrEvent;
use bimap::BiMap;
use chrono::{DateTime, Utc};
use itertools::Itertools;
use strum::EnumIter;
#[cfg(test)]
use {
crate::models::lidarr_models::NewItemMonitorType,
crate::models::lidarr_models::{MonitorType, NewItemMonitorType},
crate::models::stateful_table::SortOption,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
add_artist_search_result, download_record, metadata_profile, metadata_profile_map,
quality_profile, root_folder, tags_map,
add_artist_search_result, album, artist, download_record, metadata_profile,
metadata_profile_map, quality_profile, root_folder, tags_map,
},
crate::network::servarr_test_utils::diskspace,
strum::{Display, EnumString, IntoEnumIterator},
@@ -30,11 +33,14 @@ 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>>,
pub albums: StatefulTable<Album>,
pub artist_info_tabs: TabState,
pub artists: StatefulTable<Artist>,
pub delete_artist_files: bool,
pub delete_files: bool,
pub disk_space_vec: Vec<DiskSpace>,
pub downloads: StatefulTable<DownloadRecord>,
pub edit_artist_modal: Option<EditArtistModal>,
@@ -51,11 +57,16 @@ pub struct LidarrData<'a> {
}
impl LidarrData<'_> {
pub fn reset_delete_artist_preferences(&mut self) {
self.delete_artist_files = false;
pub fn reset_delete_preferences(&mut self) {
self.delete_files = false;
self.add_import_list_exclusion = false;
}
pub fn reset_artist_info_tabs(&mut self) {
self.albums = StatefulTable::default();
self.artist_info_tabs.index = 0;
}
pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String {
tag_ids
.iter()
@@ -68,28 +79,36 @@ impl LidarrData<'_> {
}
pub fn sorted_quality_profile_names(&self) -> Vec<String> {
let mut quality_profile_names: Vec<String> =
self.quality_profile_map.right_values().cloned().collect();
quality_profile_names.sort();
quality_profile_names
self
.quality_profile_map
.iter()
.sorted_by_key(|(id, _)| *id)
.map(|(_, name)| name)
.cloned()
.collect()
}
pub fn sorted_metadata_profile_names(&self) -> Vec<String> {
let mut metadata_profile_names: Vec<String> =
self.metadata_profile_map.right_values().cloned().collect();
metadata_profile_names.sort();
metadata_profile_names
self
.metadata_profile_map
.iter()
.sorted_by_key(|(id, _)| *id)
.map(|(_, name)| name)
.cloned()
.collect()
}
}
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,
albums: StatefulTable::default(),
artists: StatefulTable::default(),
delete_artist_files: false,
delete_files: false,
disk_space_vec: Vec::new(),
downloads: StatefulTable::default(),
edit_artist_modal: None,
@@ -108,6 +127,12 @@ impl<'a> Default for LidarrData<'a> {
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
config: None,
}]),
artist_info_tabs: TabState::new(vec![TabRoute {
title: "Albums".to_string(),
route: ActiveLidarrBlock::ArtistDetails.into(),
contextual_help: Some(&ARTIST_DETAILS_CONTEXT_CLUES),
config: None,
}]),
}
}
}
@@ -115,6 +140,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(),
@@ -132,15 +176,18 @@ impl LidarrData<'_> {
.set_items(vec![metadata_profile().name]);
let mut lidarr_data = LidarrData {
delete_artist_files: true,
delete_files: true,
disk_space_vec: vec![diskspace()],
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()
};
lidarr_data.artists.set_items(vec![Artist::default()]);
lidarr_data.albums.set_items(vec![album()]);
lidarr_data.albums.search = Some("album search".into());
lidarr_data.artists.set_items(vec![artist()]);
lidarr_data.artists.sorting(vec![SortOption {
name: "Name",
cmp_fn: Some(|a: &Artist, b: &Artist| a.artist_name.text.cmp(&b.artist_name.text)),
@@ -164,10 +211,25 @@ impl LidarrData<'_> {
pub enum ActiveLidarrBlock {
#[default]
Artists,
ArtistDetails,
ArtistsSortPrompt,
AddArtistAlreadyInLibrary,
AddArtistConfirmPrompt,
AddArtistEmptySearchResults,
AddArtistPrompt,
AddArtistSearchInput,
AddArtistSearchResults,
AddArtistSelectMetadataProfile,
AddArtistSelectMonitor,
AddArtistSelectMonitorNewItems,
AddArtistSelectQualityProfile,
AddArtistSelectRootFolder,
AddArtistTagsInput,
AutomaticallySearchArtistPrompt,
DeleteAlbumPrompt,
DeleteAlbumConfirmPrompt,
DeleteAlbumToggleDeleteFile,
DeleteAlbumToggleAddListExclusion,
DeleteArtistPrompt,
DeleteArtistConfirmPrompt,
DeleteArtistToggleDeleteFile,
@@ -182,9 +244,12 @@ pub enum ActiveLidarrBlock {
EditArtistToggleMonitored,
FilterArtists,
FilterArtistsError,
SearchAlbums,
SearchAlbumsError,
SearchArtists,
SearchArtistsError,
UpdateAllArtistsPrompt,
UpdateAndScanArtistPrompt,
}
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
@@ -197,10 +262,37 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::UpdateAllArtistsPrompt,
];
pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 3] = [
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::SearchAlbums,
ActiveLidarrBlock::SearchAlbumsError,
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
];
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] = [
@@ -216,6 +308,19 @@ pub const DELETE_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::DeleteArtistConfirmPrompt],
];
pub static DELETE_ALBUM_BLOCKS: [ActiveLidarrBlock; 4] = [
ActiveLidarrBlock::DeleteAlbumPrompt,
ActiveLidarrBlock::DeleteAlbumConfirmPrompt,
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile,
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion,
];
pub const DELETE_ALBUM_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::DeleteAlbumToggleDeleteFile],
&[ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion],
&[ActiveLidarrBlock::DeleteAlbumConfirmPrompt],
];
pub static EDIT_ARTIST_BLOCKS: [ActiveLidarrBlock; 8] = [
ActiveLidarrBlock::EditArtistPrompt,
ActiveLidarrBlock::EditArtistConfirmPrompt,
@@ -1,9 +1,13 @@
#[cfg(test)]
mod tests {
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
use crate::models::lidarr_models::Album;
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, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS,
DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
};
use crate::models::{
BlockSelectionState, Route,
@@ -31,19 +35,31 @@ mod tests {
}
#[test]
fn test_reset_delete_artist_preferences() {
fn test_reset_delete_preferences() {
let mut lidarr_data = LidarrData {
delete_artist_files: true,
delete_files: true,
add_import_list_exclusion: true,
..LidarrData::default()
};
lidarr_data.reset_delete_artist_preferences();
lidarr_data.reset_delete_preferences();
assert!(!lidarr_data.delete_artist_files);
assert!(!lidarr_data.delete_files);
assert!(!lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_reset_artist_info_tabs() {
let mut lidarr_data = LidarrData::default();
lidarr_data.albums.set_items(vec![Album::default()]);
lidarr_data.artist_info_tabs.index = 1;
lidarr_data.reset_artist_info_tabs();
assert_is_empty!(lidarr_data.albums);
assert_eq!(lidarr_data.artist_info_tabs.index, 0);
}
#[test]
fn test_tag_ids_to_display() {
let mut tags_map = BiMap::new();
@@ -64,17 +80,17 @@ mod tests {
#[test]
fn test_sorted_quality_profile_names() {
let mut quality_profile_map = BiMap::new();
quality_profile_map.insert(3, "test 3".to_owned());
quality_profile_map.insert(3, "test 1".to_owned());
quality_profile_map.insert(2, "test 2".to_owned());
quality_profile_map.insert(1, "test 1".to_owned());
quality_profile_map.insert(1, "test 3".to_owned());
let lidarr_data = LidarrData {
quality_profile_map,
..LidarrData::default()
};
let expected_quality_profile_vec = vec![
"test 1".to_owned(),
"test 2".to_owned(),
"test 3".to_owned(),
"test 2".to_owned(),
"test 1".to_owned(),
];
assert_iter_eq!(
@@ -86,17 +102,17 @@ mod tests {
#[test]
fn test_sorted_metadata_profile_names() {
let mut metadata_profile_map = BiMap::new();
metadata_profile_map.insert(3, "test 3".to_owned());
metadata_profile_map.insert(3, "test 1".to_owned());
metadata_profile_map.insert(2, "test 2".to_owned());
metadata_profile_map.insert(1, "test 1".to_owned());
metadata_profile_map.insert(1, "test 3".to_owned());
let lidarr_data = LidarrData {
metadata_profile_map,
..LidarrData::default()
};
let expected_metadata_profile_vec = vec![
"test 1".to_owned(),
"test 2".to_owned(),
"test 3".to_owned(),
"test 2".to_owned(),
"test 1".to_owned(),
];
assert_iter_eq!(
@@ -112,8 +128,9 @@ mod tests {
assert_none!(lidarr_data.add_artist_search);
assert!(!lidarr_data.add_import_list_exclusion);
assert_none!(lidarr_data.add_searched_artists);
assert_is_empty!(lidarr_data.albums);
assert_is_empty!(lidarr_data.artists);
assert!(!lidarr_data.delete_artist_files);
assert!(!lidarr_data.delete_files);
assert_is_empty!(lidarr_data.disk_space_vec);
assert_is_empty!(lidarr_data.downloads);
assert_none!(lidarr_data.edit_artist_modal);
@@ -139,6 +156,18 @@ mod tests {
&ARTISTS_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[0].config);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
assert_eq!(
lidarr_data.artist_info_tabs.tabs[0].route,
ActiveLidarrBlock::ArtistDetails.into()
);
assert_some_eq_x!(
&lidarr_data.artist_info_tabs.tabs[0].contextual_help,
&ARTIST_DETAILS_CONTEXT_CLUES
);
assert_none!(lidarr_data.artist_info_tabs.tabs[0].config);
}
#[test]
@@ -153,12 +182,66 @@ mod tests {
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::UpdateAllArtistsPrompt));
}
#[test]
fn test_artist_details_blocks_contains_expected_blocks() {
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 5);
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistDetails));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchArtistPrompt));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbums));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumsError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
}
#[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]
@@ -189,6 +272,34 @@ mod tests {
assert_none!(delete_artist_block_iter.next());
}
#[test]
fn test_delete_album_blocks_contents() {
assert_eq!(DELETE_ALBUM_BLOCKS.len(), 4);
assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumPrompt));
assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumConfirmPrompt));
assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumToggleDeleteFile));
assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion));
}
#[test]
fn test_delete_album_selection_blocks_ordering() {
let mut delete_album_block_iter = DELETE_ALBUM_SELECTION_BLOCKS.iter();
assert_eq!(
delete_album_block_iter.next().unwrap(),
&[ActiveLidarrBlock::DeleteAlbumToggleDeleteFile]
);
assert_eq!(
delete_album_block_iter.next().unwrap(),
&[ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion]
);
assert_eq!(
delete_album_block_iter.next().unwrap(),
&[ActiveLidarrBlock::DeleteAlbumConfirmPrompt]
);
assert_none!(delete_album_block_iter.next());
}
#[test]
fn test_edit_artist_blocks() {
assert_eq!(EDIT_ARTIST_BLOCKS.len(), 8);
+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() {
@@ -23,6 +23,7 @@ use crate::models::{
use crate::network::radarr_network::RadarrEvent;
use bimap::BiMap;
use chrono::{DateTime, Utc};
use itertools::Itertools;
use serde_json::Number;
use strum::EnumIter;
#[cfg(test)]
@@ -112,9 +113,13 @@ impl RadarrData<'_> {
}
pub fn sorted_quality_profile_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.quality_profile_map.right_values().cloned().collect();
names.sort();
names
self
.quality_profile_map
.iter()
.sorted_by_key(|(id, _)| *id)
.map(|(_, name)| name)
.cloned()
.collect()
}
}
@@ -82,17 +82,17 @@ mod tests {
#[test]
fn test_sorted_quality_profile_names() {
let mut quality_profile_map = BiMap::new();
quality_profile_map.insert(3, "test 3".to_owned());
quality_profile_map.insert(3, "test 1".to_owned());
quality_profile_map.insert(2, "test 2".to_owned());
quality_profile_map.insert(1, "test 1".to_owned());
quality_profile_map.insert(1, "test 3".to_owned());
let radarr_data = RadarrData {
quality_profile_map,
..RadarrData::default()
};
let expected_quality_profile_vec = vec![
"test 1".to_owned(),
"test 2".to_owned(),
"test 3".to_owned(),
"test 2".to_owned(),
"test 1".to_owned(),
];
assert_iter_eq!(
+15 -6
View File
@@ -25,6 +25,7 @@ use crate::{
};
use bimap::BiMap;
use chrono::{DateTime, Utc};
use itertools::Itertools;
use serde_json::Number;
use strum::EnumIter;
#[cfg(test)]
@@ -119,15 +120,23 @@ impl SonarrData<'_> {
}
pub fn sorted_quality_profile_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.quality_profile_map.right_values().cloned().collect();
names.sort();
names
self
.quality_profile_map
.iter()
.sorted_by_key(|(id, _)| *id)
.map(|(_, name)| name)
.cloned()
.collect()
}
pub fn sorted_language_profile_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.language_profiles_map.right_values().cloned().collect();
names.sort();
names
self
.language_profiles_map
.iter()
.sorted_by_key(|(id, _)| *id)
.map(|(_, name)| name)
.cloned()
.collect()
}
}
@@ -98,17 +98,17 @@ mod tests {
#[test]
fn test_sorted_quality_profile_names() {
let mut quality_profile_map = BiMap::new();
quality_profile_map.insert(3, "test 3".to_owned());
quality_profile_map.insert(3, "test 1".to_owned());
quality_profile_map.insert(2, "test 2".to_owned());
quality_profile_map.insert(1, "test 1".to_owned());
quality_profile_map.insert(1, "test 3".to_owned());
let sonarr_data = SonarrData {
quality_profile_map,
..SonarrData::default()
};
let expected_quality_profile_vec = vec![
"test 1".to_owned(),
"test 2".to_owned(),
"test 3".to_owned(),
"test 2".to_owned(),
"test 1".to_owned(),
];
assert_iter_eq!(
@@ -120,17 +120,17 @@ mod tests {
#[test]
fn test_sorted_language_profile_names() {
let mut language_profiles_map = BiMap::new();
language_profiles_map.insert(3, "test 3".to_owned());
language_profiles_map.insert(3, "test 1".to_owned());
language_profiles_map.insert(2, "test 2".to_owned());
language_profiles_map.insert(1, "test 1".to_owned());
language_profiles_map.insert(1, "test 3".to_owned());
let sonarr_data = SonarrData {
language_profiles_map,
..SonarrData::default()
};
let expected_language_profiles_vec = vec![
"test 1".to_owned(),
"test 2".to_owned(),
"test 3".to_owned(),
"test 2".to_owned(),
"test 1".to_owned(),
];
assert_iter_eq!(
+2 -2
View File
@@ -31,7 +31,7 @@ pub struct AddSeriesBody {
pub root_folder_path: String,
pub quality_profile_id: i64,
pub language_profile_id: i64,
pub series_type: String,
pub series_type: SeriesType,
pub season_folder: bool,
pub tags: Vec<i64>,
#[serde(skip_serializing, skip_deserializing)]
@@ -68,7 +68,7 @@ pub struct AddSeriesSearchResultStatistics {
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddSeriesOptions {
pub monitor: String,
pub monitor: SeriesMonitor,
pub search_for_cutoff_unmet_episodes: bool,
pub search_for_missing_episodes: bool,
}
@@ -0,0 +1,131 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{Album, DeleteParams, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::ALBUM_JSON;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use mockito::Matcher;
use pretty_assertions::assert_eq;
use serde_json::{Value, json};
#[tokio::test]
async fn test_handle_get_albums_event() {
let albums_json = json!([{
"id": 1,
"title": "Test Album",
"foreignAlbumId": "test-foreign-album-id",
"monitored": true,
"anyReleaseOk": true,
"profileId": 1,
"duration": 180,
"albumType": "Album",
"genres": ["Classical"],
"ratings": {"votes": 15, "value": 8.4},
"releaseDate": "2023-01-01T00:00:00Z",
"statistics": {
"trackFileCount": 10,
"trackCount": 10,
"totalTrackCount": 10,
"sizeOnDisk": 1024,
"percentOfTracks": 99.9
}
}]);
let response: Vec<Album> = serde_json::from_value(albums_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(albums_json)
.query("artistId=1")
.build_for(LidarrEvent::GetAlbums(1))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network.handle_lidarr_event(LidarrEvent::GetAlbums(1)).await;
mock.assert_async().await;
let LidarrSerdeable::Albums(albums) = result.unwrap() else {
panic!("Expected Albums");
};
assert_eq!(albums, response);
assert!(!app.lock().await.data.lidarr_data.albums.is_empty());
}
#[tokio::test]
async fn test_handle_delete_album_event() {
let delete_album_params = DeleteParams {
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::DeleteAlbum(delete_album_params.clone()))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::DeleteAlbum(delete_album_params))
.await
.is_ok()
);
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_toggle_album_monitoring_event() {
let mut expected_body: Value = serde_json::from_str(ALBUM_JSON).unwrap();
*expected_body.get_mut("monitored").unwrap() = json!(false);
let (get_mock, app, mut server) = MockServarrApi::get()
.returns(serde_json::from_str(ALBUM_JSON).unwrap())
.path("/1")
.build_for(LidarrEvent::GetAlbums(1))
.await;
let put_mock = server
.mock("PUT", "/api/v1/album/1")
.match_body(Matcher::Json(expected_body))
.match_header("X-Api-Key", "test1234")
.with_status(202)
.create_async()
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::ToggleAlbumMonitoring(1))
.await
);
get_mock.assert_async().await;
put_mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_album_details_event() {
let expected_album: Album = serde_json::from_str(ALBUM_JSON).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(serde_json::from_str(ALBUM_JSON).unwrap())
.path("/1")
.build_for(LidarrEvent::GetAlbumDetails(1))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetAlbumDetails(1))
.await;
mock.assert_async().await;
let LidarrSerdeable::Album(album) = result.unwrap() else {
panic!("Expected Album");
};
assert_eq!(album, expected_album);
}
}
@@ -0,0 +1,153 @@
use crate::models::lidarr_models::{Album, DeleteParams};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use log::{debug, info, warn};
use serde_json::{Value, json};
#[cfg(test)]
#[path = "lidarr_albums_network_tests.rs"]
mod lidarr_albums_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn get_albums(
&mut self,
artist_id: i64,
) -> Result<Vec<Album>> {
info!("Fetching albums for Lidarr artist with ID: {artist_id}");
let event = LidarrEvent::GetAlbums(artist_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("artistId={artist_id}")),
)
.await;
self
.handle_request::<(), Vec<Album>>(request_props, |mut albums_vec, mut app| {
albums_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.lidarr_data.albums.set_items(albums_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_album_details(
&mut self,
album_id: i64,
) -> Result<Album> {
info!("Fetching details for Lidarr album with ID: {album_id}");
let event = LidarrEvent::GetAlbumDetails(album_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
Some(format!("/{album_id}")),
None,
)
.await;
self
.handle_request::<(), Album>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn delete_album(
&mut self,
delete_album_params: DeleteParams,
) -> Result<()> {
let event = LidarrEvent::DeleteAlbum(DeleteParams::default());
let DeleteParams {
id,
delete_files,
add_import_list_exclusion,
} = delete_album_params;
info!(
"Deleting Lidarr album 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 toggle_album_monitoring(
&mut self,
album_id: i64,
) -> Result<()> {
let event = LidarrEvent::ToggleAlbumMonitoring(album_id);
info!("Toggling album monitoring for album with ID: {album_id}");
info!("Fetching album details for album with ID: {album_id}");
let detail_event = LidarrEvent::GetAlbums(0);
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{album_id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_album_body, _| {
response = detailed_album_body.to_string()
})
.await?;
info!("Constructing toggle album monitoring body");
match serde_json::from_str::<Value>(&response) {
Ok(mut detailed_album_body) => {
let monitored = detailed_album_body
.get("monitored")
.unwrap()
.as_bool()
.unwrap();
*detailed_album_body.get_mut("monitored").unwrap() = json!(!monitored);
debug!("Toggle album monitoring body: {detailed_album_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_album_body),
Some(format!("/{album_id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
Err(_) => {
warn!("Request for detailed album body was interrupted");
Ok(())
}
}
}
}
@@ -1,8 +1,8 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{
AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams, LidarrSerdeable,
NewItemMonitorType,
AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams,
LidarrSerdeable, MonitorType, NewItemMonitorType,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::network::NetworkResource;
@@ -54,7 +54,7 @@ mod tests {
#[tokio::test]
async fn test_handle_delete_artist_event() {
let delete_artist_params = DeleteArtistParams {
let delete_artist_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
@@ -134,11 +134,10 @@ mod tests {
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::ToggleArtistMonitoring(1))
.await
.is_ok()
);
get_mock.assert_async().await;
@@ -167,6 +166,52 @@ mod tests {
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_update_and_scan_artist_event() {
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "RefreshArtist",
"artistId": 1
}))
.returns(json!({}))
.build_for(LidarrEvent::UpdateAndScanArtist(1))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::UpdateAndScanArtist(1))
.await
.is_ok()
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_trigger_automatic_artist_search_event() {
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "ArtistSearch",
"artistId": 1
}))
.returns(json!({}))
.build_for(LidarrEvent::TriggerAutomaticArtistSearch(1))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::TriggerAutomaticArtistSearch(1))
.await
.is_ok()
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_artist_event() {
let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap();
@@ -439,4 +484,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;
}
}
@@ -0,0 +1,398 @@
use anyhow::Result;
use log::{debug, info, warn};
use serde_json::{Value, json};
use crate::models::Route;
use crate::models::lidarr_models::{
AddArtistBody, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, LidarrCommandBody,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::StatefulTable;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use urlencoding::encode;
#[cfg(test)]
#[path = "lidarr_artists_network_tests.rs"]
mod lidarr_artists_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn delete_artist(
&mut self,
delete_artist_params: DeleteParams,
) -> Result<()> {
let event = LidarrEvent::DeleteArtist(DeleteParams::default());
let DeleteParams {
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;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Artist>>(request_props, |mut artists_vec, mut app| {
if !matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _)
) {
artists_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.lidarr_data.artists.set_items(artists_vec);
app.data.lidarr_data.artists.apply_sorting_toggle(false);
}
})
.await
}
pub(in crate::network::lidarr_network) async fn get_artist_details(
&mut self,
artist_id: i64,
) -> Result<Artist> {
info!("Fetching details for Lidarr artist with ID: {artist_id}");
let event = LidarrEvent::GetArtistDetails(artist_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
Some(format!("/{artist_id}")),
None,
)
.await;
self
.handle_request::<(), Artist>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn toggle_artist_monitoring(
&mut self,
artist_id: i64,
) -> Result<()> {
let event = LidarrEvent::ToggleArtistMonitoring(artist_id);
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
info!("Toggling artist monitoring for artist with ID: {artist_id}");
info!("Fetching artist details for artist with ID: {artist_id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{artist_id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
response = detailed_artist_body.to_string()
})
.await?;
info!("Constructing toggle artist monitoring body");
match serde_json::from_str::<Value>(&response) {
Ok(mut detailed_artist_body) => {
let monitored = detailed_artist_body
.get("monitored")
.unwrap()
.as_bool()
.unwrap();
*detailed_artist_body.get_mut("monitored").unwrap() = json!(!monitored);
debug!("Toggle artist monitoring body: {detailed_artist_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_artist_body),
Some(format!("/{artist_id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
Err(_) => {
warn!("Request for detailed artist body was interrupted");
Ok(())
}
}
}
pub(in crate::network::lidarr_network) async fn update_all_artists(&mut self) -> Result<Value> {
info!("Updating all artists");
let event = LidarrEvent::UpdateAllArtists;
let body = LidarrCommandBody {
name: "RefreshArtist".to_owned(),
..LidarrCommandBody::default()
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<LidarrCommandBody, Value>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn update_and_scan_artist(
&mut self,
artist_id: i64,
) -> Result<Value> {
let event = LidarrEvent::UpdateAndScanArtist(artist_id);
info!("Updating and scanning artist with ID: {artist_id}");
let body = LidarrCommandBody {
name: "RefreshArtist".to_owned(),
artist_id: Some(artist_id),
..LidarrCommandBody::default()
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<LidarrCommandBody, Value>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn trigger_automatic_artist_search(
&mut self,
artist_id: i64,
) -> Result<Value> {
let event = LidarrEvent::TriggerAutomaticArtistSearch(artist_id);
info!("Searching indexers for artist with ID: {artist_id}");
let body = LidarrCommandBody {
name: "ArtistSearch".to_owned(),
artist_id: Some(artist_id),
..LidarrCommandBody::default()
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<LidarrCommandBody, Value>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn search_artist(
&mut self,
query: String,
) -> Result<Vec<AddArtistSearchResult>> {
info!("Searching for artist: {query}");
let event = LidarrEvent::SearchNewArtist(String::new());
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("term={}", encode(&query))),
)
.await;
let result = self
.handle_request::<(), Vec<AddArtistSearchResult>>(request_props, |artist_vec, mut app| {
if artist_vec.is_empty() {
app.pop_and_push_navigation_stack(ActiveLidarrBlock::AddArtistEmptySearchResults.into());
} else if let Some(add_searched_artists) =
app.data.lidarr_data.add_searched_artists.as_mut()
{
add_searched_artists.set_items(artist_vec);
} else {
let mut add_searched_artists = StatefulTable::default();
add_searched_artists.set_items(artist_vec);
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
}
})
.await;
if result.is_err() {
self.app.lock().await.data.lidarr_data.add_searched_artists = Some(StatefulTable::default());
}
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,
) -> Result<()> {
info!("Editing Lidarr artist");
if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() {
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
edit_artist_params.tags = Some(tag_ids_vec);
}
let artist_id = edit_artist_params.artist_id;
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
let event = LidarrEvent::EditArtist(EditArtistParams::default());
info!("Fetching artist details for artist with ID: {artist_id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{artist_id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
response = detailed_artist_body.to_string()
})
.await?;
info!("Constructing edit artist body");
let mut detailed_artist_body: Value = serde_json::from_str(&response)?;
let (
monitored,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags,
) = {
let monitored = edit_artist_params.monitored.unwrap_or(
detailed_artist_body["monitored"]
.as_bool()
.expect("Unable to deserialize 'monitored'"),
);
let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| {
serde_json::from_value(detailed_artist_body["monitorNewItems"].clone())
.expect("Unable to deserialize 'monitorNewItems'")
});
let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| {
detailed_artist_body["qualityProfileId"]
.as_i64()
.expect("Unable to deserialize 'qualityProfileId'")
});
let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| {
detailed_artist_body["metadataProfileId"]
.as_i64()
.expect("Unable to deserialize 'metadataProfileId'")
});
let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| {
detailed_artist_body["path"]
.as_str()
.expect("Unable to deserialize 'path'")
.to_owned()
});
let tags = if edit_artist_params.clear_tags {
vec![]
} else {
edit_artist_params.tags.unwrap_or(
detailed_artist_body["tags"]
.as_array()
.expect("Unable to deserialize 'tags'")
.iter()
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
.collect(),
)
};
(
monitored,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags,
)
};
*detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored);
*detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items);
*detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
*detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id);
*detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path);
*detailed_artist_body.get_mut("tags").unwrap() = json!(tags);
debug!("Edit artist body: {detailed_artist_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_artist_body),
Some(format!("/{artist_id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
}
+2 -330
View File
@@ -1,330 +1,2 @@
use anyhow::Result;
use log::{debug, info, warn};
use serde_json::{Value, json};
use crate::models::Route;
use crate::models::lidarr_models::{
AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_models::CommandBody;
use crate::models::stateful_table::StatefulTable;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use urlencoding::encode;
#[cfg(test)]
#[path = "lidarr_library_network_tests.rs"]
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;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Artist>>(request_props, |mut artists_vec, mut app| {
if !matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _)
) {
artists_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.lidarr_data.artists.set_items(artists_vec);
app.data.lidarr_data.artists.apply_sorting_toggle(false);
}
})
.await
}
pub(in crate::network::lidarr_network) async fn get_artist_details(
&mut self,
artist_id: i64,
) -> Result<Artist> {
info!("Fetching details for Lidarr artist with ID: {artist_id}");
let event = LidarrEvent::GetArtistDetails(artist_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
Some(format!("/{artist_id}")),
None,
)
.await;
self
.handle_request::<(), Artist>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn toggle_artist_monitoring(
&mut self,
artist_id: i64,
) -> Result<()> {
let event = LidarrEvent::ToggleArtistMonitoring(artist_id);
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
info!("Toggling artist monitoring for artist with ID: {artist_id}");
info!("Fetching artist details for artist with ID: {artist_id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{artist_id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
response = detailed_artist_body.to_string()
})
.await?;
info!("Constructing toggle artist monitoring body");
match serde_json::from_str::<Value>(&response) {
Ok(mut detailed_artist_body) => {
let monitored = detailed_artist_body
.get("monitored")
.unwrap()
.as_bool()
.unwrap();
*detailed_artist_body.get_mut("monitored").unwrap() = json!(!monitored);
debug!("Toggle artist monitoring body: {detailed_artist_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_artist_body),
Some(format!("/{artist_id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
Err(_) => {
warn!("Request for detailed artist body was interrupted");
Ok(())
}
}
}
pub(in crate::network::lidarr_network) async fn update_all_artists(&mut self) -> Result<Value> {
info!("Updating all artists");
let event = LidarrEvent::UpdateAllArtists;
let body = CommandBody {
name: "RefreshArtist".to_owned(),
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn search_artist(
&mut self,
query: String,
) -> Result<Vec<AddArtistSearchResult>> {
info!("Searching for artist: {query}");
let event = LidarrEvent::SearchNewArtist(String::new());
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("term={}", encode(&query))),
)
.await;
let result = self
.handle_request::<(), Vec<AddArtistSearchResult>>(request_props, |artist_vec, mut app| {
if artist_vec.is_empty() {
app.pop_and_push_navigation_stack(ActiveLidarrBlock::AddArtistEmptySearchResults.into());
} else if let Some(add_searched_artists) =
app.data.lidarr_data.add_searched_artists.as_mut()
{
add_searched_artists.set_items(artist_vec);
} else {
let mut add_searched_artists = StatefulTable::default();
add_searched_artists.set_items(artist_vec);
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
}
})
.await;
if result.is_err() {
self.app.lock().await.data.lidarr_data.add_searched_artists = Some(StatefulTable::default());
}
result
}
pub(in crate::network::lidarr_network) async fn edit_artist(
&mut self,
mut edit_artist_params: EditArtistParams,
) -> Result<()> {
info!("Editing Lidarr artist");
if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() {
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
edit_artist_params.tags = Some(tag_ids_vec);
}
let artist_id = edit_artist_params.artist_id;
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
let event = LidarrEvent::EditArtist(EditArtistParams::default());
info!("Fetching artist details for artist with ID: {artist_id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{artist_id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
response = detailed_artist_body.to_string()
})
.await?;
info!("Constructing edit artist body");
let mut detailed_artist_body: Value = serde_json::from_str(&response)?;
let (
monitored,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags,
) = {
let monitored = edit_artist_params.monitored.unwrap_or(
detailed_artist_body["monitored"]
.as_bool()
.expect("Unable to deserialize 'monitored'"),
);
let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| {
serde_json::from_value(detailed_artist_body["monitorNewItems"].clone())
.expect("Unable to deserialize 'monitorNewItems'")
});
let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| {
detailed_artist_body["qualityProfileId"]
.as_i64()
.expect("Unable to deserialize 'qualityProfileId'")
});
let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| {
detailed_artist_body["metadataProfileId"]
.as_i64()
.expect("Unable to deserialize 'metadataProfileId'")
});
let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| {
detailed_artist_body["path"]
.as_str()
.expect("Unable to deserialize 'path'")
.to_owned()
});
let tags = if edit_artist_params.clear_tags {
vec![]
} else {
edit_artist_params.tags.unwrap_or(
detailed_artist_body["tags"]
.as_array()
.expect("Unable to deserialize 'tags'")
.iter()
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
.collect(),
)
};
(
monitored,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags,
)
};
*detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored);
*detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items);
*detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
*detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id);
*detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path);
*detailed_artist_body.get_mut("tags").unwrap() = json!(tags);
debug!("Edit artist body: {detailed_artist_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_artist_body),
Some(format!("/{artist_id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
}
mod albums;
mod artists;
@@ -3,9 +3,9 @@
pub mod test_utils {
use crate::models::HorizontallyScrollableText;
use crate::models::lidarr_models::{
AddArtistSearchResult, Artist, ArtistStatistics, ArtistStatus, DownloadRecord, DownloadStatus,
DownloadsResponse, EditArtistParams, Member, MetadataProfile, NewItemMonitorType, Ratings,
SystemStatus,
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, Member, MetadataProfile,
NewItemMonitorType, Ratings, SystemStatus,
};
use crate::models::servarr_models::{QualityProfile, RootFolder, Tag};
use bimap::BiMap;
@@ -51,6 +51,27 @@ pub mod test_utils {
}
}"#;
pub const ALBUM_JSON: &str = r#"{
"id": 1,
"title": "Test Album",
"foreignAlbumId": "test-foreign-album-id",
"monitored": true,
"anyReleaseOk": true,
"profileId": 1,
"duration": 180,
"albumType": "Album",
"genres": ["Classical"],
"ratings": {"votes": 15, "value": 8.4},
"releaseDate": "2023-01-01T00:00:00Z",
"statistics": {
"trackFileCount": 10,
"trackCount": 10,
"totalTrackCount": 10,
"sizeOnDisk": 1024,
"percentOfTracks": 99.9
}
}"#;
pub fn member() -> Member {
Member {
name: Some("alex".to_owned()),
@@ -199,4 +220,33 @@ pub mod test_utils {
ratings: Some(ratings()),
}
}
pub fn album_statistics() -> AlbumStatistics {
AlbumStatistics {
track_file_count: 10,
track_count: 10,
total_track_count: 10,
size_on_disk: 1024,
percent_of_tracks: 99.9,
}
}
pub fn album() -> Album {
Album {
id: 1,
title: "Test Album".into(),
foreign_album_id: "test-foreign-album-id".to_string(),
monitored: true,
any_release_ok: true,
profile_id: 1,
duration: 180,
album_type: Some("Album".to_owned()),
genres: vec!["Classical".to_owned()],
ratings: Some(ratings()),
release_date: Some(DateTime::from(
DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap(),
)),
statistics: Some(album_statistics()),
}
}
}
@@ -1,7 +1,9 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::models::lidarr_models::{LidarrSerdeable, MetadataProfile};
use crate::models::lidarr_models::{
AddArtistBody, DeleteParams, EditArtistParams, 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,7 +20,10 @@ mod tests {
#[values(
LidarrEvent::GetArtistDetails(0),
LidarrEvent::ListArtists,
LidarrEvent::ToggleArtistMonitoring(0)
LidarrEvent::AddArtist(AddArtistBody::default()),
LidarrEvent::ToggleArtistMonitoring(0),
LidarrEvent::DeleteArtist(DeleteParams::default()),
LidarrEvent::EditArtist(EditArtistParams::default())
)]
event: LidarrEvent,
) {
@@ -45,10 +50,30 @@ mod tests {
}
#[rstest]
fn test_resource_command(#[values(LidarrEvent::UpdateAllArtists)] event: LidarrEvent) {
fn test_resource_command(
#[values(
LidarrEvent::UpdateAllArtists,
LidarrEvent::TriggerAutomaticArtistSearch(0),
LidarrEvent::UpdateAndScanArtist(0)
)]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/command");
}
#[rstest]
fn test_resource_album(
#[values(
LidarrEvent::GetAlbums(0),
LidarrEvent::ToggleAlbumMonitoring(0),
LidarrEvent::GetAlbumDetails(0),
LidarrEvent::DeleteAlbum(DeleteParams::default())
)]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/album");
}
#[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
#[case(LidarrEvent::GetDownloads(500), "/queue")]
+40 -3
View File
@@ -3,7 +3,7 @@ use log::info;
use super::{NetworkEvent, NetworkResource};
use crate::models::lidarr_models::{
DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
AddArtistBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
};
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
@@ -23,10 +23,14 @@ pub mod lidarr_network_test_utils;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum LidarrEvent {
AddArtist(AddArtistBody),
AddTag(String),
DeleteArtist(DeleteArtistParams),
DeleteAlbum(DeleteParams),
DeleteArtist(DeleteParams),
DeleteTag(i64),
EditArtist(EditArtistParams),
GetAlbums(i64),
GetAlbumDetails(i64),
GetArtistDetails(i64),
GetDiskSpace,
GetDownloads(u64),
@@ -40,8 +44,11 @@ pub enum LidarrEvent {
HealthCheck,
ListArtists,
SearchNewArtist(String),
ToggleAlbumMonitoring(i64),
ToggleArtistMonitoring(i64),
TriggerAutomaticArtistSearch(i64),
UpdateAllArtists,
UpdateAndScanArtist(i64),
}
impl NetworkResource for LidarrEvent {
@@ -52,11 +59,18 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::EditArtist(_)
| LidarrEvent::GetArtistDetails(_)
| LidarrEvent::ListArtists
| LidarrEvent::AddArtist(_)
| LidarrEvent::ToggleArtistMonitoring(_) => "/artist",
LidarrEvent::GetAlbums(_)
| LidarrEvent::ToggleAlbumMonitoring(_)
| LidarrEvent::GetAlbumDetails(_)
| LidarrEvent::DeleteAlbum(_) => "/album",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) => "/queue",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::UpdateAllArtists => "/command",
LidarrEvent::TriggerAutomaticArtistSearch(_)
| LidarrEvent::UpdateAllArtists
| LidarrEvent::UpdateAndScanArtist(_) => "/command",
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
LidarrEvent::GetQualityProfiles => "/qualityprofile",
LidarrEvent::GetRootFolders => "/rootfolder",
@@ -80,6 +94,9 @@ impl Network<'_, '_> {
) -> Result<LidarrSerdeable> {
match lidarr_event {
LidarrEvent::AddTag(tag) => self.add_lidarr_tag(tag).await.map(LidarrSerdeable::from),
LidarrEvent::DeleteAlbum(params) => {
self.delete_album(params).await.map(LidarrSerdeable::from)
}
LidarrEvent::DeleteArtist(params) => {
self.delete_artist(params).await.map(LidarrSerdeable::from)
}
@@ -87,10 +104,17 @@ impl Network<'_, '_> {
.delete_lidarr_tag(tag_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetAlbums(artist_id) => {
self.get_albums(artist_id).await.map(LidarrSerdeable::from)
}
LidarrEvent::GetArtistDetails(artist_id) => self
.get_artist_details(artist_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetAlbumDetails(album_id) => self
.get_album_details(album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from),
LidarrEvent::GetDownloads(count) => self
.get_lidarr_downloads(count)
@@ -126,12 +150,25 @@ impl Network<'_, '_> {
LidarrEvent::SearchNewArtist(query) => {
self.search_artist(query).await.map(LidarrSerdeable::from)
}
LidarrEvent::ToggleAlbumMonitoring(album_id) => self
.toggle_album_monitoring(album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::ToggleArtistMonitoring(artist_id) => self
.toggle_artist_monitoring(artist_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::TriggerAutomaticArtistSearch(artist_id) => self
.trigger_automatic_artist_search(artist_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from),
LidarrEvent::UpdateAndScanArtist(artist_id) => self
.update_and_scan_artist(artist_id)
.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),
}
}
@@ -2,7 +2,7 @@
mod tests {
use crate::models::radarr_models::{
AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, EditMovieParams,
MinimumAvailability, Movie, MovieHistoryItem, RadarrReleaseDownloadBody,
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrReleaseDownloadBody,
};
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
@@ -50,13 +50,13 @@ mod tests {
tmdb_id: 1234,
title: "Test".to_owned(),
root_folder_path: "/nfs2".to_owned(),
minimum_availability: "announced".to_owned(),
minimum_availability: MinimumAvailability::Announced,
monitored: true,
quality_profile_id: 2222,
tags: vec![1, 2],
tag_input_string: Some("usenet, testing".into()),
add_options: AddMovieOptions {
monitor: "movieOnly".to_owned(),
monitor: MovieMonitor::MovieOnly,
search_for_movie: true,
},
};
@@ -99,13 +99,13 @@ mod tests {
tmdb_id: 1234,
title: "Test".to_owned(),
root_folder_path: "/nfs2".to_owned(),
minimum_availability: "announced".to_owned(),
minimum_availability: MinimumAvailability::Announced,
monitored: true,
quality_profile_id: 2222,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddMovieOptions {
monitor: "movieOnly".to_owned(),
monitor: MovieMonitor::MovieOnly,
search_for_movie: true,
},
};
@@ -2,8 +2,8 @@
mod tests {
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::sonarr_models::{
AddSeriesBody, AddSeriesOptions, DeleteSeriesParams, EditSeriesParams, Series, SeriesType,
SonarrHistoryItem, SonarrSerdeable,
AddSeriesBody, AddSeriesOptions, DeleteSeriesParams, EditSeriesParams, Series, SeriesMonitor,
SeriesType, SonarrHistoryItem, SonarrSerdeable,
};
use crate::models::stateful_table::{SortOption, StatefulTable};
use crate::network::NetworkResource;
@@ -27,12 +27,12 @@ mod tests {
root_folder_path: "/nfs2".to_owned(),
quality_profile_id: 2222,
language_profile_id: 2222,
series_type: "standard".to_owned(),
series_type: SeriesType::Standard,
season_folder: true,
tags: Vec::new(),
tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddSeriesOptions {
monitor: "all".to_owned(),
monitor: SeriesMonitor::All,
search_for_cutoff_unmet_episodes: true,
search_for_missing_episodes: true,
},
@@ -82,12 +82,12 @@ mod tests {
root_folder_path: "/nfs2".to_owned(),
quality_profile_id: 2222,
language_profile_id: 2222,
series_type: "standard".to_owned(),
series_type: SeriesType::Standard,
season_folder: true,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddSeriesOptions {
monitor: "all".to_owned(),
monitor: SeriesMonitor::All,
search_for_cutoff_unmet_episodes: true,
search_for_missing_episodes: true,
},
+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,329 @@
use chrono::Utc;
use deunicode::deunicode;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use regex::Regex;
use crate::app::App;
use crate::models::Route;
use crate::models::lidarr_models::Album;
use crate::models::servarr_data::lidarr::lidarr_data::{ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock};
use crate::ui::lidarr_ui::library::delete_album_ui::DeleteAlbumUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{
borderless_block, get_width_from_percentage, layout_block_top_border, title_block,
};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs};
use crate::utils::convert_to_gb;
#[cfg(test)]
#[path = "artist_details_ui_tests.rs"]
mod artist_details_ui_tests;
pub(super) struct ArtistDetailsUi;
impl DrawUi for ArtistDetailsUi {
fn accepts(route: Route) -> bool {
let Route::Lidarr(active_lidarr_block, _) = route else {
return false;
};
DeleteAlbumUi::accepts(route) || ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let route = app.get_current_route();
if let Route::Lidarr(active_lidarr_block, _) = route {
let draw_artist_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| {
f.render_widget(
title_block(
&app
.data
.lidarr_data
.artists
.current_selection()
.artist_name
.text,
),
popup_area,
);
let [description_area, detail_area] =
Layout::vertical([Constraint::Percentage(37), Constraint::Fill(0)])
.margin(1)
.areas(popup_area);
draw_artist_description(f, app, description_area);
let content_area = draw_tabs(
f,
detail_area,
"Artist Details",
&app.data.lidarr_data.artist_info_tabs,
);
draw_artist_details(f, app, content_area);
match active_lidarr_block {
_ if DeleteAlbumUi::accepts(route) => DeleteAlbumUi::draw(f, app, area),
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
let prompt = format!(
"Do you want to trigger an automatic search of your indexers for all monitored album(s) for the artist: {}?",
app.data.lidarr_data.artists.current_selection().artist_name
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Automatic Artist Search")
.prompt(&prompt)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
let prompt = format!(
"Do you want to trigger an update and disk scan for the artist: {}?",
app.data.lidarr_data.artists.current_selection().artist_name
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Update and Scan")
.prompt(&prompt)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
_ => (),
}
};
draw_popup(f, app, draw_artist_details_popup, Size::XXLarge);
}
}
}
fn draw_artist_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let current_selection = app.data.lidarr_data.artists.current_selection();
let monitored = if current_selection.monitored {
"Yes"
} else {
"No"
};
let quality_profile = app
.data
.lidarr_data
.quality_profile_map
.get_by_left(&current_selection.quality_profile_id)
.cloned()
.unwrap_or_default();
let metadata_profile = app
.data
.lidarr_data
.metadata_profile_map
.get_by_left(&current_selection.metadata_profile_id)
.cloned()
.unwrap_or_default();
let overview = Regex::new(r"[\r\n\t]")
.unwrap()
.replace_all(
&deunicode(
current_selection
.overview
.as_ref()
.unwrap_or(&String::new()),
),
"",
)
.to_string();
let mut artist_description = vec![
Line::from(vec![
"Artist: ".primary().bold(),
current_selection.artist_name.text.clone().primary().bold(),
]),
Line::from(vec![
"Overview: ".primary().bold(),
overview.default_color(),
]),
Line::from(vec![
"Disambiguation: ".primary().bold(),
current_selection
.disambiguation
.clone()
.unwrap_or_default()
.default_color(),
]),
Line::from(vec![
"Type: ".primary().bold(),
current_selection
.artist_type
.clone()
.unwrap_or_default()
.default_color(),
]),
Line::from(vec![
"Status: ".primary().bold(),
current_selection.status.to_display_str().default_color(),
]),
Line::from(vec![
"Genres: ".primary().bold(),
current_selection.genres.join(", ").default_color(),
]),
Line::from(vec![
"Rating: ".primary().bold(),
current_selection
.ratings
.as_ref()
.map_or_else(
|| "N/A".to_owned(),
|r| format!("{}%", (r.value * 10.0) as i32),
)
.default_color(),
]),
Line::from(vec![
"Path: ".primary().bold(),
current_selection.path.clone().default_color(),
]),
Line::from(vec![
"Quality Profile: ".primary().bold(),
quality_profile.default_color(),
]),
Line::from(vec![
"Metadata Profile: ".primary().bold(),
metadata_profile.default_color(),
]),
Line::from(vec![
"Monitored: ".primary().bold(),
monitored.default_color(),
]),
];
if let Some(stats) = current_selection.statistics.as_ref() {
let size = convert_to_gb(stats.size_on_disk);
artist_description.extend(vec![
Line::from(vec![
"Albums: ".primary().bold(),
stats.album_count.to_string().default_color(),
]),
Line::from(vec![
"Tracks: ".primary().bold(),
format!("{}/{}", stats.track_file_count, stats.total_track_count).default_color(),
]),
Line::from(vec![
"Size on Disk: ".primary().bold(),
format!("{size:.2} GB").default_color(),
]),
]);
}
let description_paragraph = Paragraph::new(artist_description)
.block(borderless_block())
.wrap(Wrap { trim: true });
f.render_widget(description_paragraph, area);
}
fn draw_artist_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Lidarr(active_lidarr_block, _) =
app.data.lidarr_data.artist_info_tabs.get_active_route()
{
match active_lidarr_block {
ActiveLidarrBlock::ArtistDetails => draw_albums_table(f, app, area),
_ => (),
}
}
}
fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let current_selection = if app.data.lidarr_data.albums.is_empty() {
Album::default()
} else {
app.data.lidarr_data.albums.current_selection().clone()
};
let content = Some(&mut app.data.lidarr_data.albums);
let album_row_mapping = |album: &Album| {
album.title.scroll_left_or_reset(
get_width_from_percentage(area, 33),
*album == current_selection,
app.ui_scroll_tick_count == 0,
);
let monitored = if album.monitored { "🏷" } else { "" };
let album_type = album.album_type.clone().unwrap_or_default();
let release_date = album
.release_date
.map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string());
let track_count = album.statistics.as_ref().map_or_else(
|| "0/0".to_owned(),
|s| format!("{}/{}", s.track_file_count, s.total_track_count),
);
let size = album
.statistics
.as_ref()
.map_or(0f64, |s| convert_to_gb(s.size_on_disk));
let duration_mins = album.duration / 60000;
let row = Row::new(vec![
Cell::from(monitored.to_owned()),
Cell::from(album.title.to_string()),
Cell::from(album_type),
Cell::from(track_count),
Cell::from(format!("{duration_mins} min")),
Cell::from(release_date),
Cell::from(format!("{size:.2} GB")),
]);
if !album.monitored {
row.unmonitored()
} else if let Some(stats) = album.statistics.as_ref() {
if stats.track_file_count == stats.total_track_count && stats.total_track_count > 0 {
row.downloaded()
} else if let Some(release_date) = album.release_date.as_ref() {
if release_date > &Utc::now() {
row.unreleased()
} else {
row.missing()
}
} else {
row.missing()
}
} else {
row.indeterminate()
}
};
let is_searching = active_lidarr_block == ActiveLidarrBlock::SearchAlbums;
let album_table = ManagarrTable::new(content, album_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.searching(is_searching)
.search_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::SearchAlbumsError)
.headers([
"Monitored",
"Title",
"Type",
"Tracks",
"Duration",
"Release Date",
"Size",
])
.constraints([
Constraint::Percentage(7),
Constraint::Percentage(35),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(13),
Constraint::Percentage(15),
]);
if is_searching {
album_table.show_cursor(f, area);
}
f.render_widget(album_table, area);
}
}
@@ -0,0 +1,144 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi;
#[test]
fn test_artist_details_ui_accepts() {
let mut blocks = ARTIST_DETAILS_BLOCKS.clone().to_vec();
blocks.extend(DELETE_ALBUM_BLOCKS);
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if blocks.contains(&active_lidarr_block) {
assert!(ArtistDetailsUi::accepts(active_lidarr_block.into()));
} else {
assert!(!ArtistDetailsUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use rstest::rstest;
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi;
use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app};
#[rstest]
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
#[case(ActiveLidarrBlock::SearchAlbums, 0)]
#[case(ActiveLidarrBlock::SearchAlbumsError, 0)]
#[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, 0)]
#[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, 0)]
fn test_artist_details_ui_renders(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.artist_info_tabs.set_index(index);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ArtistDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(
format!("artist_details_{active_lidarr_block}_{index}"),
output
);
}
#[rstest]
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
fn test_artist_details_ui_renders_artist_details_loading(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.artist_info_tabs.set_index(index);
app.is_loading = true;
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ArtistDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(
format!("loading_artist_details_{active_lidarr_block}"),
output
);
}
#[rstest]
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
fn test_artist_details_ui_renders_artist_details_empty(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
) {
let mut app = App::test_default_fully_populated();
app.data.lidarr_data.albums = StatefulTable::default();
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.artist_info_tabs.set_index(index);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ArtistDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(
format!("empty_artist_details_{active_lidarr_block}"),
output
);
}
#[test]
fn test_artist_details_ui_renders_delete_album_prompt_over_artist_details() {
let mut app = App::test_default_fully_populated();
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ArtistDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_artist_details_ui_renders_update_and_scan_prompt_over_artist_details() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ArtistDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_artist_details_ui_renders_automatic_search_prompt_over_artist_details() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ArtistDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
@@ -0,0 +1,57 @@
use ratatui::Frame;
use ratatui::layout::Rect;
use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ALBUM_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::popup::{Popup, Size};
#[cfg(test)]
#[path = "delete_album_ui_tests.rs"]
mod delete_album_ui_tests;
pub(in crate::ui::lidarr_ui) struct DeleteAlbumUi;
impl DrawUi for DeleteAlbumUi {
fn accepts(route: Route) -> bool {
let Route::Lidarr(active_lidarr_block, _) = route else {
return false;
};
DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
if matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::DeleteAlbumPrompt, _)
) {
let selected_block = app.data.lidarr_data.selected_block.get_active_block();
let prompt = format!(
"Do you really want to delete the album: \n{}?",
app.data.lidarr_data.albums.current_selection().title.text
);
let checkboxes = vec![
Checkbox::new("Delete Album Files")
.checked(app.data.lidarr_data.delete_files)
.highlighted(selected_block == ActiveLidarrBlock::DeleteAlbumToggleDeleteFile),
Checkbox::new("Add List Exclusion")
.checked(app.data.lidarr_data.add_import_list_exclusion)
.highlighted(selected_block == ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion),
];
let confirmation_prompt = ConfirmationPrompt::new()
.title("Delete Album")
.prompt(&prompt)
.checkboxes(checkboxes)
.yes_no_highlighted(selected_block == ActiveLidarrBlock::DeleteAlbumConfirmPrompt)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
}
}
@@ -0,0 +1,43 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::delete_album_ui::DeleteAlbumUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_delete_album_ui_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) {
assert!(DeleteAlbumUi::accepts(active_lidarr_block.into()));
} else {
assert!(!DeleteAlbumUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use super::*;
#[test]
fn test_delete_album_ui_renders_delete_album() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DeleteAlbumUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ impl DrawUi for DeleteArtistUi {
);
let checkboxes = vec![
Checkbox::new("Delete Artist Files")
.checked(app.data.lidarr_data.delete_artist_files)
.checked(app.data.lidarr_data.delete_files)
.highlighted(selected_block == ActiveLidarrBlock::DeleteArtistToggleDeleteFile),
Checkbox::new("Add List Exclusion")
.checked(app.data.lidarr_data.add_import_list_exclusion)
+24 -3
View File
@@ -7,10 +7,13 @@ use ratatui::widgets::ListItem;
use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS,
};
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::render_selectable_input_box;
use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi;
use crate::ui::utils::title_block_centered;
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
@@ -34,7 +37,13 @@ impl DrawUi for EditArtistUi {
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
if let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() {
if let Route::Lidarr(active_lidarr_block, context_option) = app.get_current_route() {
if let Some(context) = context_option
&& ARTIST_DETAILS_BLOCKS.contains(&context)
{
draw_popup(f, app, ArtistDetailsUi::draw, Size::Large);
}
let draw_edit_artist_prompt = |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| {
draw_edit_artist_confirmation_prompt(f, app, prompt_area);
@@ -66,7 +75,19 @@ fn draw_edit_artist_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar
.artist_name
.text
.clone();
let title = format!("Edit - {artist_name}");
let artist_disambiguation = app
.data
.lidarr_data
.artists
.current_selection()
.disambiguation
.clone()
.unwrap_or_default();
let title = if artist_disambiguation.is_empty() {
format!("Edit - {artist_name}")
} else {
format!("Edit - {artist_name} ({artist_disambiguation})")
};
f.render_widget(title_block_centered(&title), area);
let yes_no_value = app.data.lidarr_data.prompt_confirm;
+49 -2
View File
@@ -4,7 +4,8 @@ mod tests {
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS,
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::{LibraryUi, decorate_artist_row_with_style};
@@ -17,8 +18,10 @@ mod tests {
let mut library_ui_blocks = Vec::new();
library_ui_blocks.extend(LIBRARY_BLOCKS);
library_ui_blocks.extend(DELETE_ARTIST_BLOCKS);
library_ui_blocks.extend(DELETE_ALBUM_BLOCKS);
library_ui_blocks.extend(EDIT_ARTIST_BLOCKS);
library_ui_blocks.extend(ADD_ARTIST_BLOCKS);
library_ui_blocks.extend(ARTIST_DETAILS_BLOCKS);
for active_lidarr_block in ActiveLidarrBlock::iter() {
if library_ui_blocks.contains(&active_lidarr_block) {
@@ -182,7 +185,7 @@ mod tests {
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS,
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
};
use rstest::rstest;
@@ -263,5 +266,49 @@ mod tests {
insta::assert_snapshot!(output);
}
#[test]
fn test_library_ui_renders_artist_details_over_library() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
LibraryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_library_ui_renders_edit_artist_over_artist_details() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
LibraryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_library_ui_renders_dropdown_over_edit_artist_over_artist_details() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
app.push_navigation_stack(ActiveLidarrBlock::EditArtistSelectMetadataProfile.into());
app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
LibraryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
+5
View File
@@ -1,4 +1,5 @@
use add_artist_ui::AddArtistUi;
use artist_details_ui::ArtistDetailsUi;
use delete_artist_ui::DeleteArtistUi;
use edit_artist_ui::EditArtistUi;
use ratatui::{
@@ -28,9 +29,11 @@ use crate::{
};
mod add_artist_ui;
mod artist_details_ui;
mod delete_artist_ui;
mod edit_artist_ui;
mod delete_album_ui;
#[cfg(test)]
#[path = "library_ui_tests.rs"]
mod library_ui_tests;
@@ -43,6 +46,7 @@ impl DrawUi for LibraryUi {
return AddArtistUi::accepts(route)
|| DeleteArtistUi::accepts(route)
|| EditArtistUi::accepts(route)
|| ArtistDetailsUi::accepts(route)
|| LIBRARY_BLOCKS.contains(&active_lidarr_block);
}
@@ -56,6 +60,7 @@ impl DrawUi for LibraryUi {
match route {
_ if AddArtistUi::accepts(route) => AddArtistUi::draw(f, app, area),
_ if DeleteArtistUi::accepts(route) => DeleteArtistUi::draw(f, app, area),
_ if ArtistDetailsUi::accepts(route) => ArtistDetailsUi::draw(f, app, area),
_ if EditArtistUi::accepts(route) => EditArtistUi::draw(f, app, area),
Route::Lidarr(ActiveLidarrBlock::UpdateAllArtistsPrompt, _) => {
let confirmation_prompt = ConfirmationPrompt::new()
@@ -1,47 +0,0 @@
---
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
expression: output
---
╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮
│Test Artist │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -1,47 +0,0 @@
---
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 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -13,8 +13,8 @@ expression: output
│Test Artist │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
✔ Name Type Status Rating Genres
=> ✔ Test Artist Person Continuing 8.4 soundtrack
│ │
│ │
│ │
@@ -26,7 +26,7 @@ expression: output
│ │
│ │
│ ╭─────────────── Error ───────────────╮ │
│ │ No artists found matching your query! │ │
│ │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 ││ │
╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -14,7 +14,7 @@ expression: output
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✔ Name Type Status Rating Genres │
│=> Test Artist Person Continuing 8.4 soundtrack │
│=> Test Artist Person Continuing 8.4 soundtrack │
│ │
│ │
│ │
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│ │
│ │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │
│Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │
│ │ for all monitored album(s) for the artist: Alex? │ │
│ │ │ │
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
││ Albums │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
││ ││ Yes ││ No ││ ││
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
││ ╰───────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│ │
│ │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ╭──────────── Error ─────────────╮ ││
││ │ No items found matching search │ ││
││ ╰──────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│ │
│ │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ╭────────────── Search ───────────────╮ ││
││ │album search │ ││
││ ╰───────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 ╭──────────────────── Update and Scan ────────────────────╮ │
│Size on Disk: 0.00 GB │ Do you want to trigger an update and disk scan for the │ │
│ │ artist: Alex? │ │
│ │ │ │
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
││ Albums │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
││ ││ Yes ││ No ││ ││
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
││ ╰───────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │
│Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │
│ │ for all monitored album(s) for the artist: Alex? │ │
│ │ │ │
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
││ Albums │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
││ ││ Yes ││ No ││ ││
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
││ ╰───────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 ╭───────────────────── Delete Album ──────────────────────╮ │
│Size on Disk: 0.00 GB │ Do you really want to delete the album: │ │
│ │ Test Album? │ │
│ │ │ │
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
││ Albums │ ╭───╮ │ ││
││─────────────────────────────────────────│ Delete Album Files: │ ✔ │ │───────────────────────────────────────────││
││ Monitored Title │ ╰───╯ │ Release Date Size ││
││=> 🏷 Test Album │ ╭───╮ │ 2023-01-01 0.00 GB ││
││ │ Add List Exclusion: │ │ │ ││
││ │ ╰───╯ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
││ ││ Yes ││ No ││ ││
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
││ ╰───────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 ╭──────────────────── Update and Scan ────────────────────╮ │
│Size on Disk: 0.00 GB │ Do you want to trigger an update and disk scan for the │ │
│ │ artist: Alex? │ │
│ │ │ │
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
││ Albums │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
││ ││ Yes ││ No ││ ││
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
││ ╰───────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│ │
│ │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│ │
│ │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ ││
││ ││
││ Loading ... ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,38 @@
---
source: src/ui/lidarr_ui/library/delete_album_ui_tests.rs
expression: output
---
╭───────────────────── Delete Album ──────────────────────╮
│ Do you really want to delete the album: │
│ Test Album? │
│ │
│ │
│ ╭───╮ │
│ Delete Album Files: │ ✔ │ │
│ ╰───╯ │
│ ╭───╮ │
│ Add List Exclusion: │ │ │
│ ╰───╯ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -20,7 +20,7 @@ expression: output
╭───────────────────── Delete Artist ─────────────────────╮
│ Do you really want to delete the artist: │
?
Alex?
│ │
│ │
│ ╭───╮ │
@@ -8,7 +8,7 @@ expression: output
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
│ │
│ │
│ │
@@ -8,7 +8,7 @@ expression: output
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
│ │
│ │
│ │
@@ -8,7 +8,7 @@ expression: output
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
│ │
│ │
│ │
@@ -8,7 +8,7 @@ expression: output
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
│ │
│ │
│ │
@@ -8,7 +8,7 @@ expression: output
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
│ │
│ │
│ │

Some files were not shown because too many files have changed in this diff Show More