This commit is contained in:
2026-01-07 10:45:49 -07:00
parent 9b4eda6a9d
commit 3c1634d1e3
65 changed files with 2355 additions and 100 deletions
+2 -1
View File
@@ -7,12 +7,13 @@ use crate::models::Route;
#[path = "lidarr_context_clues_tests.rs"]
mod lidarr_context_clues_tests;
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 8] = [
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 9] = [
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
@@ -24,6 +24,10 @@ mod tests {
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
+146
View File
@@ -0,0 +1,146 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, ArgGroup, Subcommand};
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command, mutex_flags_or_option},
models::lidarr_models::{EditArtistParams, NewItemMonitorType},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "edit_command_handler_tests.rs"]
mod edit_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrEditCommand {
#[command(
about = "Edit preferences for the specified artist",
group(
ArgGroup::new("edit_artist")
.args([
"enable_monitoring",
"disable_monitoring",
"monitor_new_items",
"quality_profile_id",
"metadata_profile_id",
"root_folder_path",
"tag",
"clear_tags"
]).required(true)
.multiple(true))
)]
Artist {
#[arg(
long,
help = "The ID of the artist whose settings you want to edit",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "Enable monitoring of this artist in Lidarr so Lidarr will automatically download releases from this artist if they are available",
conflicts_with = "disable_monitoring"
)]
enable_monitoring: bool,
#[arg(
long,
help = "Disable monitoring of this artist so Lidarr does not automatically download releases from this artist if they are available",
conflicts_with = "enable_monitoring"
)]
disable_monitoring: bool,
#[arg(
long,
help = "How Lidarr should monitor new albums from this artist",
value_enum
)]
monitor_new_items: Option<NewItemMonitorType>,
#[arg(long, help = "The ID of the quality profile to use for this artist")]
quality_profile_id: Option<i64>,
#[arg(long, help = "The ID of the metadata profile to use for this artist")]
metadata_profile_id: Option<i64>,
#[arg(
long,
help = "The root folder path where all artist data and metadata should live"
)]
root_folder_path: Option<String>,
#[arg(
long,
help = "Tag IDs to tag this artist with",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(long, help = "Clear all tags on this artist", conflicts_with = "tag")]
clear_tags: bool,
},
}
impl From<LidarrEditCommand> for Command {
fn from(value: LidarrEditCommand) -> Self {
Command::Lidarr(LidarrCommand::Edit(value))
}
}
pub(super) struct LidarrEditCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrEditCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrEditCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrEditCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrEditCommand::Artist {
artist_id,
enable_monitoring,
disable_monitoring,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tag,
clear_tags,
} => {
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
let edit_artist_params = EditArtistParams {
artist_id,
monitored: monitored_value,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags: tag,
tag_input_string: None,
clear_tags,
};
self
.network
.handle_network_event(LidarrEvent::EditArtist(edit_artist_params).into())
.await?;
"Artist Updated".to_owned()
}
};
Ok(result)
}
}
@@ -0,0 +1,409 @@
#[cfg(test)]
mod tests {
use crate::cli::{
Command,
lidarr::{LidarrCommand, edit_command_handler::LidarrEditCommand},
};
#[test]
fn test_lidarr_edit_command_from() {
let command = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: false,
monitor_new_items: None,
quality_profile_id: None,
metadata_profile_id: None,
root_folder_path: None,
tag: None,
clear_tags: false,
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Edit(command)));
}
mod cli {
use crate::{Cli, models::lidarr_models::NewItemMonitorType};
use super::*;
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_edit_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_artist_with_artist_id_still_requires_arguments() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_artist_monitoring_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--enable-monitoring",
"--disable-monitoring",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_artist_tag_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--tag",
"1",
"--clear-tags",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[rstest]
fn test_edit_artist_assert_argument_flags_require_args(
#[values(
"--monitor-new-items",
"--quality-profile-id",
"--metadata-profile-id",
"--root-folder-path",
"--tag"
)]
flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
flag,
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_artist_monitor_new_items_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--monitor-new-items",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_artist_only_requires_at_least_one_argument_plus_artist_id() {
let expected_args = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: false,
monitor_new_items: None,
quality_profile_id: None,
metadata_profile_id: None,
root_folder_path: Some("/nfs/test".to_owned()),
tag: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--root-folder-path",
"/nfs/test",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_artist_tag_argument_is_repeatable() {
let expected_args = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: false,
monitor_new_items: None,
quality_profile_id: None,
metadata_profile_id: None,
root_folder_path: None,
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_artist_all_arguments_defined() {
let expected_args = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: true,
disable_monitoring: false,
monitor_new_items: Some(NewItemMonitorType::New),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--enable-monitoring",
"--monitor-new-items",
"new",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
"--root-folder-path",
"/nfs/test",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
CliCommandHandler,
lidarr::edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler},
},
models::{
Serdeable,
lidarr_models::{EditArtistParams, LidarrSerdeable, NewItemMonitorType},
},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_edit_artist_command() {
let expected_edit_artist_params = EditArtistParams {
artist_id: 1,
monitored: Some(true),
monitor_new_items: Some(NewItemMonitorType::New),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_artist_command = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: true,
disable_monitoring: false,
monitor_new_items: Some(NewItemMonitorType::New),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_edit_artist_command_handles_disable_monitoring_flag_properly() {
let expected_edit_artist_params = EditArtistParams {
artist_id: 1,
monitored: Some(false),
monitor_new_items: Some(NewItemMonitorType::None),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_artist_command = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: true,
monitor_new_items: Some(NewItemMonitorType::None),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_edit_artist_command_no_monitoring_boolean_flags_returns_none_value() {
let expected_edit_artist_params = EditArtistParams {
artist_id: 1,
monitored: None,
monitor_new_items: Some(NewItemMonitorType::All),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_artist_command = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: false,
monitor_new_items: Some(NewItemMonitorType::All),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
+12
View File
@@ -3,6 +3,7 @@ use std::sync::Arc;
use anyhow::Result;
use clap::{Subcommand, arg};
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler};
use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
use list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
@@ -14,6 +15,7 @@ use crate::{app::App, network::NetworkTrait};
use super::{CliCommandHandler, Command};
mod delete_command_handler;
mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod refresh_command_handler;
@@ -29,6 +31,11 @@ pub enum LidarrCommand {
about = "Commands to delete resources from your Lidarr instance"
)]
Delete(LidarrDeleteCommand),
#[command(
subcommand,
about = "Commands to edit resources in your Lidarr instance"
)]
Edit(LidarrEditCommand),
#[command(
subcommand,
about = "Commands to fetch details of the resources in your Lidarr instance"
@@ -89,6 +96,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
.handle()
.await?
}
LidarrCommand::Edit(edit_command) => {
LidarrEditCommandHandler::with(self.app, edit_command, self.network)
.handle()
.await?
}
LidarrCommand::Get(get_command) => {
LidarrGetCommandHandler::with(self.app, get_command, self.network)
.handle()
+2 -1
View File
@@ -2,6 +2,7 @@ use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::models::Route;
use crate::models::servarr_data::ActiveKeybindingBlock;
#[cfg(test)]
@@ -75,7 +76,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandle
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -7,6 +7,7 @@ use crate::{
matches_key,
models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS},
};
use crate::models::Route;
#[cfg(test)]
#[path = "delete_artist_handler_tests.rs"]
@@ -143,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler<
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,455 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::{Route, Scrollable};
use crate::models::lidarr_models::EditArtistParams;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::network::lidarr_network::LidarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)]
#[path = "edit_artist_handler_tests.rs"]
mod edit_artist_handler_tests;
pub(in crate::handlers::lidarr_handlers) struct EditArtistHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl EditArtistHandler<'_, '_> {
fn build_edit_artist_params(&mut self) -> EditArtistParams {
let edit_artist_modal = self
.app
.data
.lidarr_data
.edit_artist_modal
.take()
.expect("EditArtistModal is None");
let artist_id = self.app.data.lidarr_data.artists.current_selection().id;
let tags = edit_artist_modal.tags.text;
let EditArtistModal {
monitored,
path,
monitor_list,
quality_profile_list,
metadata_profile_list,
..
} = edit_artist_modal;
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();
EditArtistParams {
artist_id,
monitored,
monitor_new_items: Some(*monitor_list.current_selection()),
quality_profile_id: Some(quality_profile_id),
metadata_profile_id: Some(metadata_profile_id),
root_folder_path: Some(path.text),
tag_input_string: Some(tags),
..EditArtistParams::default()
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditArtistHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
EDIT_ARTIST_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>,
) -> EditArtistHandler<'a, 'b> {
EditArtistHandler {
key,
app,
active_lidarr_block: active_block,
context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading && self.app.data.lidarr_data.edit_artist_modal.is_some()
}
fn handle_scroll_up(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_up(),
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_up(),
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_up(),
ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_down(),
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_down(),
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_down(),
ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.down(),
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_top(),
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_top(),
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_top(),
ActiveLidarrBlock::EditArtistPathInput => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.path
.scroll_home(),
ActiveLidarrBlock::EditArtistTagsInput => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.tags
.scroll_home(),
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_bottom(),
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::EditArtistPathInput => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.path
.reset_offset(),
ActiveLidarrBlock::EditArtistTagsInput => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.tags
.reset_offset(),
_ => (),
}
}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistPrompt => handle_prompt_toggle(self.app, self.key),
ActiveLidarrBlock::EditArtistPathInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.path
)
}
ActiveLidarrBlock::EditArtistTagsInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.tags
)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistPrompt => {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::EditArtistConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::EditArtist(self.build_edit_artist_params()));
self.app.should_refresh = true;
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
| ActiveLidarrBlock::EditArtistSelectQualityProfile
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.push_navigation_stack(
(
self.app.data.lidarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
),
ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => {
self.app.push_navigation_stack(
(
self.app.data.lidarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
);
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveLidarrBlock::EditArtistToggleMonitored => {
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitored = Some(
!self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitored
.unwrap_or_default(),
)
}
_ => (),
}
}
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
| ActiveLidarrBlock::EditArtistSelectQualityProfile
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(),
ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistTagsInput | ActiveLidarrBlock::EditArtistPathInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveLidarrBlock::EditArtistPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.edit_artist_modal = None;
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
| ActiveLidarrBlock::EditArtistSelectQualityProfile
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(),
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistPathInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.path
)
}
ActiveLidarrBlock::EditArtistTagsInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.tags
)
}
ActiveLidarrBlock::EditArtistPrompt => {
if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::EditArtistConfirmPrompt
&& matches_key!(confirm, key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::EditArtist(self.build_edit_artist_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,215 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
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::edit_artist_handler::EditArtistHandler;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
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::EDIT_ARTIST_SELECTION_BLOCKS;
use super::*;
#[rstest]
fn test_edit_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
let mut app = App::test_default();
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle();
if key == Key::Up {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::EditArtistToggleMonitored
);
} else {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::EditArtistSelectQualityProfile
);
}
}
#[rstest]
fn test_edit_artist_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.edit_artist_modal = Some(EditArtistModal::default());
app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle();
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
);
}
}
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.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::EDIT_ARTIST_SELECTION_BLOCKS;
use super::*;
use crate::assert_navigation_popped;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_edit_artist_prompt_prompt_decline_submit() {
let mut app = App::test_default();
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
// Navigate to the confirm prompt (last selection block)
for _ in 0..EDIT_ARTIST_SELECTION_BLOCKS.len() - 1 {
app.data.lidarr_data.selected_block.down();
}
EditArtistHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::EditArtistPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm_action.is_none());
assert_navigation_popped!(&app, ActiveLidarrBlock::Artists.into());
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_edit_artist_prompt_esc() {
let mut app = App::test_default();
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
EditArtistHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle();
assert_navigation_popped!(&app, ActiveLidarrBlock::Artists.into());
assert!(app.data.lidarr_data.edit_artist_modal.is_none());
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[test]
fn test_edit_artist_select_blocks_esc() {
let mut app = App::test_default();
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
app.push_navigation_stack(ActiveLidarrBlock::EditArtistSelectQualityProfile.into());
EditArtistHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::EditArtistSelectQualityProfile,
None,
)
.handle();
assert_navigation_popped!(&app, ActiveLidarrBlock::EditArtistPrompt.into());
}
}
#[test]
fn test_edit_artist_handler_accepts() {
let mut edit_artist_handler_blocks = Vec::new();
for block in ActiveLidarrBlock::iter() {
if EditArtistHandler::accepts(block) {
edit_artist_handler_blocks.push(block);
}
}
assert_eq!(edit_artist_handler_blocks, EDIT_ARTIST_BLOCKS.to_vec());
}
#[test]
fn test_edit_artist_handler_is_not_ready_when_loading() {
let mut app = App::test_default();
app.is_loading = true;
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
let handler = EditArtistHandler::new(
Key::Esc,
&mut app,
ActiveLidarrBlock::EditArtistPrompt,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_edit_artist_handler_is_not_ready_when_edit_artist_modal_is_none() {
let mut app = App::test_default();
app.data.lidarr_data.edit_artist_modal = None;
let handler = EditArtistHandler::new(
Key::Esc,
&mut app,
ActiveLidarrBlock::EditArtistPrompt,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_edit_artist_handler_is_ready_when_not_loading_and_modal_is_some() {
let mut app = App::test_default();
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
let handler = EditArtistHandler::new(
Key::Esc,
&mut app,
ActiveLidarrBlock::EditArtistPrompt,
None,
);
assert!(handler.is_ready());
}
}
@@ -11,19 +11,24 @@ mod tests {
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::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS};
use crate::network::lidarr_network::LidarrEvent;
use crate::{assert_modal_absent, assert_navigation_popped, assert_navigation_pushed};
#[test]
fn test_library_handler_accepts() {
for lidarr_block in ActiveLidarrBlock::iter() {
if LIBRARY_BLOCKS.contains(&lidarr_block) {
let mut library_handler_blocks = Vec::new();
library_handler_blocks.extend(LIBRARY_BLOCKS);
library_handler_blocks.extend(DELETE_ARTIST_BLOCKS);
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
ActiveLidarrBlock::iter().for_each(|lidarr_block| {
if library_handler_blocks.contains(&lidarr_block) {
assert!(LibraryHandler::accepts(lidarr_block));
} else {
assert!(!LibraryHandler::accepts(lidarr_block));
}
}
});
}
#[test]
+31 -6
View File
@@ -7,8 +7,10 @@ use crate::{
BlockSelectionState,
lidarr_models::Artist,
servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
LIBRARY_BLOCKS,
},
servarr_data::lidarr::modals::EditArtistModal,
stateful_table::SortOption,
},
network::lidarr_network::LidarrEvent,
@@ -18,8 +20,11 @@ use super::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
mod delete_artist_handler;
mod edit_artist_handler;
pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler;
pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler;
use crate::models::Route;
#[cfg(test)]
#[path = "library_handler_tests.rs"]
@@ -29,7 +34,7 @@ pub(super) struct LibraryHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
context: Option<ActiveLidarrBlock>,
}
impl LibraryHandler<'_, '_> {
@@ -55,12 +60,23 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
|app| &mut app.data.lidarr_data.artists,
artists_table_handling_config,
) {
self.handle_key_event();
match self.active_lidarr_block {
_ if DeleteArtistHandler::accepts(self.active_lidarr_block) => {
DeleteArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if EditArtistHandler::accepts(self.active_lidarr_block) => {
EditArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ => self.handle_key_event(),
}
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
LIBRARY_BLOCKS.contains(&active_block)
DeleteArtistHandler::accepts(active_block)
|| EditArtistHandler::accepts(active_block)
|| LIBRARY_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
@@ -77,7 +93,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
key,
app,
active_lidarr_block: active_block,
_context: context,
context,
}
}
@@ -151,6 +167,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
_ if matches_key!(edit, key) => {
self.app.data.lidarr_data.edit_artist_modal =
Some((&self.app.data.lidarr_data).into());
self
.app
.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
}
_ if matches_key!(update, key) => {
self
.app
@@ -177,7 +202,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -1,11 +1,47 @@
#[cfg(test)]
mod tests {
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::LidarrHandler;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
#[rstest]
fn test_lidarr_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 = LidarrHandler::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_lidarr_handler_is_ready() {
let mut app = App::test_default();
app.is_loading = true;
let handler = LidarrHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::default(),
None,
);
assert!(handler.is_ready());
}
#[test]
fn test_lidarr_handler_accepts() {
for lidarr_block in ActiveLidarrBlock::iter() {
+3 -7
View File
@@ -1,9 +1,9 @@
use library::{DeleteArtistHandler, LibraryHandler};
use library::LibraryHandler;
use crate::{
app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
};
use crate::models::Route;
use super::KeyEventHandler;
mod library;
@@ -22,10 +22,6 @@ pub(super) struct LidarrHandler<'a, 'b> {
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b> {
fn handle(&mut self) {
match self.active_lidarr_block {
_ if DeleteArtistHandler::accepts(self.active_lidarr_block) => {
DeleteArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if LibraryHandler::accepts(self.active_lidarr_block) => {
LibraryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
@@ -85,7 +81,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -5,6 +5,7 @@ use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key;
use crate::models::radarr_models::BlocklistItem;
use crate::models::Route;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption;
use crate::network::radarr_network::RadarrEvent;
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -3,7 +3,7 @@ use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::matches_key;
use crate::models::BlockSelectionState;
use crate::models::{BlockSelectionState, Route};
use crate::models::servarr_data::radarr::radarr_data::{
ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS,
@@ -148,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -1,7 +1,7 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::Scrollable;
use crate::models::{Route, Scrollable};
use crate::models::radarr_models::EditCollectionParams;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
@@ -376,7 +376,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -6,7 +6,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key;
use crate::models::BlockSelectionState;
use crate::models::{BlockSelectionState, Route};
use crate::models::radarr_models::Collection;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
@@ -179,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key;
use crate::models::Route;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
use crate::network::radarr_network::RadarrEvent;
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -8,6 +8,7 @@ use crate::network::radarr_network::RadarrEvent;
use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
use crate::models::Route;
#[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"]
@@ -527,7 +528,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -9,6 +9,7 @@ use crate::network::radarr_network::RadarrEvent;
use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
use crate::models::Route;
#[cfg(test)]
#[path = "edit_indexer_settings_handler_tests.rs"]
@@ -293,7 +294,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestA
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key;
use crate::models::BlockSelectionState;
use crate::models::{BlockSelectionState, Route};
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
@@ -212,7 +212,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -2,6 +2,7 @@ use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::models::Route;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
#[cfg(test)]
@@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -7,7 +7,7 @@ use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{
ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock,
};
use crate::models::{BlockSelectionState, Scrollable};
use crate::models::{BlockSelectionState, Route, Scrollable};
use crate::network::radarr_network::RadarrEvent;
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
@@ -558,7 +558,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -3,6 +3,7 @@ use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
use crate::models::radarr_models::DeleteMovieParams;
use crate::models::Route;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS};
use crate::network::radarr_network::RadarrEvent;
@@ -141,7 +142,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -1,7 +1,7 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::Scrollable;
use crate::models::{Route, Scrollable};
use crate::models::radarr_models::EditMovieParams;
use crate::models::servarr_data::radarr::modals::EditMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS};
@@ -397,7 +397,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -11,7 +11,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
};
use crate::models::servarr_models::Language;
use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, Scrollable};
use crate::models::{BlockSelectionState, Route, Scrollable};
use crate::network::radarr_network::RadarrEvent;
#[cfg(test)]
@@ -379,7 +379,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -1
View File
@@ -8,6 +8,7 @@ use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
use crate::handlers::radarr_handlers::system::SystemHandler;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::{App, Key, matches_key};
use crate::models::Route;
mod blocklist;
mod collections;
@@ -112,7 +113,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -3,7 +3,7 @@ use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::models::HorizontallyScrollableText;
use crate::models::{HorizontallyScrollableText, Route};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS};
use crate::models::servarr_models::AddRootFolderBody;
use crate::network::radarr_network::RadarrEvent;
@@ -231,7 +231,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
use crate::handlers::{KeyEventHandler, handle_clear_errors};
use crate::matches_key;
use crate::models::Scrollable;
use crate::models::{Route, Scrollable};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
mod system_details_handler;
@@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -2,7 +2,7 @@ use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
use crate::models::Scrollable;
use crate::models::{Route, Scrollable};
use crate::models::radarr_models::RadarrTaskName;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::models::stateful_list::StatefulList;
@@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key;
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS};
use crate::models::sonarr_models::BlocklistItem;
use crate::models::stateful_table::SortOption;
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key;
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
use crate::network::sonarr_network::SonarrEvent;
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -1
View File
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors};
use crate::matches_key;
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS};
use crate::models::servarr_models::Language;
use crate::models::sonarr_models::SonarrHistoryItem;
@@ -121,7 +122,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -8,6 +8,7 @@ use crate::network::sonarr_network::SonarrEvent;
use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
use crate::models::Route;
#[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"]
@@ -526,7 +527,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -7,6 +7,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{
use crate::models::sonarr_models::IndexerSettings;
use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_prompt_left_right_keys, matches_key};
use crate::models::Route;
#[cfg(test)]
#[path = "edit_indexer_settings_handler_tests.rs"]
@@ -202,7 +203,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ use crate::handlers::sonarr_handlers::indexers::test_all_indexers_handler::TestA
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key;
use crate::models::BlockSelectionState;
use crate::models::{BlockSelectionState, Route};
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
@@ -211,7 +211,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -2,6 +2,7 @@ use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
#[cfg(test)]
@@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -5,7 +5,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{
ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, ActiveSonarrBlock,
};
use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult};
use crate::models::{BlockSelectionState, Scrollable};
use crate::models::{BlockSelectionState, Route, Scrollable};
use crate::network::sonarr_network::SonarrEvent;
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
@@ -625,7 +625,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -7,6 +7,7 @@ use crate::{
matches_key,
models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS},
};
use crate::models::Route;
#[cfg(test)]
#[path = "delete_series_handler_tests.rs"]
@@ -143,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -1,7 +1,7 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::Scrollable;
use crate::models::{Route, Scrollable};
use crate::models::servarr_data::sonarr::modals::EditSeriesModal;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS};
use crate::models::sonarr_models::EditSeriesParams;
@@ -471,7 +471,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::library::season_details_handler::releases_
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS};
use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody};
use crate::network::sonarr_network::SonarrEvent;
@@ -370,7 +371,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandle
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -1
View File
@@ -25,6 +25,7 @@ use crate::handlers::sonarr_handlers::library::episode_details_handler::EpisodeD
use crate::handlers::sonarr_handlers::library::season_details_handler::SeasonDetailsHandler;
use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::models::Route;
mod add_series_handler;
mod delete_series_handler;
@@ -245,7 +246,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -12,6 +12,7 @@ use crate::models::sonarr_models::{
use crate::models::stateful_table::SortOption;
use crate::network::sonarr_network::SonarrEvent;
use serde_json::Number;
use crate::models::Route;
#[cfg(test)]
#[path = "season_details_handler_tests.rs"]
@@ -458,7 +459,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -4,7 +4,7 @@ use crate::handlers::sonarr_handlers::history::history_sorting_options;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
use crate::models::BlockSelectionState;
use crate::models::{BlockSelectionState, Route};
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS,
};
@@ -342,7 +342,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ use system::SystemHandler;
use crate::{
app::App, event::Key, matches_key, models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
};
use crate::models::Route;
use super::KeyEventHandler;
mod blocklist;
@@ -115,7 +115,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -3,7 +3,7 @@ use crate::event::Key;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::models::HorizontallyScrollableText;
use crate::models::{HorizontallyScrollableText, Route};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS};
use crate::models::servarr_models::AddRootFolderBody;
use crate::network::sonarr_network::SonarrEvent;
@@ -229,7 +229,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<'
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::sonarr_handlers::system::system_details_handler::SystemDetailsHandler;
use crate::handlers::{KeyEventHandler, handle_clear_errors};
use crate::matches_key;
use crate::models::Scrollable;
use crate::models::{Route, Scrollable};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
mod system_details_handler;
@@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemHandler<'a, 'b
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -2,7 +2,7 @@ use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
use crate::models::Scrollable;
use crate::models::{Route, Scrollable};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::models::sonarr_models::SonarrTaskName;
use crate::models::stateful_list::StatefulList;
@@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+2 -1
View File
@@ -14,6 +14,7 @@ mod tests {
use crate::models::servarr_models::Language;
use crate::models::stateful_table::SortOption;
use rstest::rstest;
use crate::models::Route;
struct TableHandlerUnit<'a, 'b> {
key: Key,
@@ -98,7 +99,7 @@ mod tests {
self.app
}
fn current_route(&self) -> crate::models::Route {
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
+17
View File
@@ -118,6 +118,7 @@ impl From<(&i64, &String)> for MetadataProfile {
Copy,
Debug,
EnumIter,
clap::ValueEnum,
Display,
EnumDisplayStyle,
)]
@@ -205,6 +206,21 @@ pub struct DeleteArtistParams {
pub add_import_list_exclusion: bool,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditArtistParams {
pub artist_id: i64,
pub monitored: Option<bool>,
pub monitor_new_items: Option<NewItemMonitorType>,
pub quality_profile_id: Option<i64>,
pub metadata_profile_id: Option<i64>,
pub root_folder_path: Option<String>,
pub tags: Option<Vec<i64>>,
#[serde(skip_serializing, skip_deserializing)]
pub tag_input_string: Option<String>,
pub clear_tags: bool,
}
impl From<LidarrSerdeable> for Serdeable {
fn from(value: LidarrSerdeable) -> Serdeable {
Serdeable::Lidarr(value)
@@ -223,6 +239,7 @@ serde_enum_from!(
RootFolders(Vec<RootFolder>),
SecurityConfig(SecurityConfig),
SystemStatus(SystemStatus),
Tag(Tag),
Tags(Vec<Tag>),
Value(Value),
}
+88 -15
View File
@@ -1,3 +1,5 @@
use serde_json::Number;
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
use crate::models::{
BlockSelectionState, Route, TabRoute, TabState,
@@ -8,9 +10,17 @@ use crate::models::{
use crate::network::lidarr_network::LidarrEvent;
use bimap::BiMap;
use chrono::{DateTime, Utc};
use strum::EnumIter;
use strum::{EnumIter};
use super::modals::EditArtistModal;
#[cfg(test)]
use strum::{Display, EnumString};
use {
strum::{Display, EnumString, IntoEnumIterator},
crate::models::lidarr_models::NewItemMonitorType,
crate::models::stateful_table::SortOption,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map,
crate::network::servarr_test_utils::diskspace,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{download_record, metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map},
};
#[cfg(test)]
#[path = "lidarr_data_tests.rs"]
@@ -22,6 +32,7 @@ pub struct LidarrData<'a> {
pub delete_artist_files: bool,
pub disk_space_vec: Vec<DiskSpace>,
pub downloads: StatefulTable<DownloadRecord>,
pub edit_artist_modal: Option<EditArtistModal>,
pub main_tabs: TabState,
pub metadata_profile_map: BiMap<i64, String>,
pub prompt_confirm: bool,
@@ -39,6 +50,31 @@ impl LidarrData<'_> {
self.delete_artist_files = false;
self.add_import_list_exclusion = false;
}
pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String {
tag_ids
.iter()
.filter_map(|id| {
let id = id.as_i64()?;
self.tags_map.get_by_left(&id).cloned()
})
.collect::<Vec<String>>()
.join(", ")
}
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
}
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
}
}
impl<'a> Default for LidarrData<'a> {
@@ -49,6 +85,7 @@ impl<'a> Default for LidarrData<'a> {
delete_artist_files: false,
disk_space_vec: Vec::new(),
downloads: StatefulTable::default(),
edit_artist_modal: None,
metadata_profile_map: BiMap::new(),
prompt_confirm: false,
prompt_confirm_action: None,
@@ -71,11 +108,25 @@ impl<'a> Default for LidarrData<'a> {
#[cfg(test)]
impl LidarrData<'_> {
pub fn test_default_fully_populated() -> Self {
use crate::models::lidarr_models::{Artist, DownloadRecord};
use crate::models::servarr_models::{DiskSpace, RootFolder};
use crate::models::stateful_table::SortOption;
let mut edit_artist_modal = EditArtistModal {
monitored: Some(true),
path: "/nfs/music".into(),
tags: "alex".into(),
..EditArtistModal::default()
};
edit_artist_modal.monitor_list.set_items(NewItemMonitorType::iter().collect());
edit_artist_modal.quality_profile_list.set_items(vec![quality_profile().name]);
edit_artist_modal.metadata_profile_list.set_items(vec![metadata_profile().name]);
let mut lidarr_data = LidarrData::default();
let mut lidarr_data = LidarrData {
delete_artist_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),
tags_map: tags_map(),
..LidarrData::default()
};
lidarr_data.artists.set_items(vec![Artist::default()]);
lidarr_data.artists.sorting(vec![SortOption {
name: "Name",
@@ -83,19 +134,12 @@ impl LidarrData<'_> {
}]);
lidarr_data.artists.search = Some("artist search".into());
lidarr_data.artists.filter = Some("artist filter".into());
lidarr_data.quality_profile_map = BiMap::from_iter([(1i64, "Lossless".to_owned())]);
lidarr_data.metadata_profile_map = BiMap::from_iter([(1i64, "Standard".to_owned())]);
lidarr_data.tags_map = BiMap::from_iter([(1i64, "usenet".to_owned())]);
lidarr_data.disk_space_vec = vec![DiskSpace {
free_space: 50000000000,
total_space: 100000000000,
}];
lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
.set_items(vec![download_record()]);
lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
.set_items(vec![root_folder()]);
lidarr_data.version = "1.0.0".to_owned();
lidarr_data
@@ -112,6 +156,14 @@ pub enum ActiveLidarrBlock {
DeleteArtistConfirmPrompt,
DeleteArtistToggleDeleteFile,
DeleteArtistToggleAddListExclusion,
EditArtistPrompt,
EditArtistConfirmPrompt,
EditArtistPathInput,
EditArtistSelectMetadataProfile,
EditArtistSelectMonitorNewItems,
EditArtistSelectQualityProfile,
EditArtistTagsInput,
EditArtistToggleMonitored,
FilterArtists,
FilterArtistsError,
SearchArtists,
@@ -142,6 +194,27 @@ pub const DELETE_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::DeleteArtistConfirmPrompt],
];
pub static EDIT_ARTIST_BLOCKS: [ActiveLidarrBlock; 8] = [
ActiveLidarrBlock::EditArtistPrompt,
ActiveLidarrBlock::EditArtistConfirmPrompt,
ActiveLidarrBlock::EditArtistPathInput,
ActiveLidarrBlock::EditArtistSelectMetadataProfile,
ActiveLidarrBlock::EditArtistSelectMonitorNewItems,
ActiveLidarrBlock::EditArtistSelectQualityProfile,
ActiveLidarrBlock::EditArtistTagsInput,
ActiveLidarrBlock::EditArtistToggleMonitored,
];
pub const EDIT_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::EditArtistToggleMonitored],
&[ActiveLidarrBlock::EditArtistSelectMonitorNewItems],
&[ActiveLidarrBlock::EditArtistSelectQualityProfile],
&[ActiveLidarrBlock::EditArtistSelectMetadataProfile],
&[ActiveLidarrBlock::EditArtistPathInput],
&[ActiveLidarrBlock::EditArtistTagsInput],
&[ActiveLidarrBlock::EditArtistConfirmPrompt],
];
impl From<ActiveLidarrBlock> for Route {
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
Route::Lidarr(active_lidarr_block, None)
@@ -1,15 +1,15 @@
#[cfg(test)]
mod tests {
use bimap::BiMap;
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
use crate::models::servarr_data::lidarr::lidarr_data::{
DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
};
use crate::models::servarr_data::lidarr::lidarr_data::{DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS};
use crate::models::{
BlockSelectionState, Route,
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS, LidarrData},
};
use chrono::{DateTime, Utc};
use pretty_assertions::{assert_eq, assert_str_eq};
use serde_json::Number;
#[test]
fn test_from_active_lidarr_block_to_route() {
@@ -41,6 +41,50 @@ mod tests {
assert!(!lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_tag_ids_to_display() {
let mut tags_map = BiMap::new();
tags_map.insert(3, "test 3".to_owned());
tags_map.insert(2, "test 2".to_owned());
tags_map.insert(1, "test 1".to_owned());
let lidarr_data = LidarrData {
tags_map,
..LidarrData::default()
};
assert_str_eq!(lidarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), "test 1, test 2");
}
#[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(2, "test 2".to_owned());
quality_profile_map.insert(1, "test 1".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()];
assert_iter_eq!(lidarr_data.sorted_quality_profile_names(), expected_quality_profile_vec);
}
#[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(2, "test 2".to_owned());
metadata_profile_map.insert(1, "test 1".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()];
assert_iter_eq!(lidarr_data.sorted_metadata_profile_names(), expected_metadata_profile_vec);
}
#[test]
fn test_lidarr_data_default() {
let lidarr_data = LidarrData::default();
@@ -50,6 +94,7 @@ mod tests {
assert!(!lidarr_data.delete_artist_files);
assert_is_empty!(lidarr_data.disk_space_vec);
assert_is_empty!(lidarr_data.downloads);
assert_none!(lidarr_data.edit_artist_modal);
assert_is_empty!(lidarr_data.metadata_profile_map);
assert!(!lidarr_data.prompt_confirm);
assert_none!(lidarr_data.prompt_confirm_action);
@@ -113,4 +158,31 @@ mod tests {
);
assert_none!(delete_artist_block_iter.next());
}
#[test]
fn test_edit_artist_blocks() {
assert_eq!(EDIT_ARTIST_BLOCKS.len(), 8);
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistPrompt));
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistConfirmPrompt));
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistPathInput));
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectMetadataProfile));
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectMonitorNewItems));
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectQualityProfile));
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistTagsInput));
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistToggleMonitored));
}
#[test]
fn test_edit_artist_selection_blocks_ordering() {
let mut edit_artist_block_iter = EDIT_ARTIST_SELECTION_BLOCKS.iter();
assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistToggleMonitored]);
assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistSelectMonitorNewItems]);
assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistSelectQualityProfile]);
assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistSelectMetadataProfile]);
assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistPathInput]);
assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistTagsInput]);
assert_eq!(edit_artist_block_iter.next().unwrap(), &[ActiveLidarrBlock::EditArtistConfirmPrompt]);
assert_none!(edit_artist_block_iter.next());
}
}
+1
View File
@@ -1 +1,2 @@
pub mod lidarr_data;
pub mod modals;
+78
View File
@@ -0,0 +1,78 @@
use strum::IntoEnumIterator;
use super::lidarr_data::LidarrData;
use crate::models::{
HorizontallyScrollableText, lidarr_models::NewItemMonitorType, stateful_list::StatefulList,
};
#[cfg(test)]
#[path = "modals_tests.rs"]
mod modals_tests;
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct EditArtistModal {
pub monitor_list: StatefulList<NewItemMonitorType>,
pub quality_profile_list: StatefulList<String>,
pub metadata_profile_list: StatefulList<String>,
pub monitored: Option<bool>,
pub path: HorizontallyScrollableText,
pub tags: HorizontallyScrollableText,
}
impl From<&LidarrData<'_>> for EditArtistModal {
fn from(lidarr_data: &LidarrData<'_>) -> EditArtistModal {
let mut edit_artist_modal = EditArtistModal::default();
let artist = lidarr_data.artists.current_selection();
edit_artist_modal
.monitor_list
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
edit_artist_modal.path = artist.path.clone().into();
edit_artist_modal.tags = lidarr_data.tag_ids_to_display(&artist.tags).into();
edit_artist_modal.monitored = Some(artist.monitored);
let monitor_index = edit_artist_modal
.monitor_list
.items
.iter()
.position(|m| *m == artist.monitor_new_items);
edit_artist_modal.monitor_list.state.select(monitor_index);
edit_artist_modal
.quality_profile_list
.set_items(lidarr_data.sorted_quality_profile_names());
let quality_profile_name = lidarr_data
.quality_profile_map
.get_by_left(&artist.quality_profile_id)
.unwrap();
let quality_profile_index = edit_artist_modal
.quality_profile_list
.items
.iter()
.position(|profile| profile == quality_profile_name);
edit_artist_modal
.quality_profile_list
.state
.select(quality_profile_index);
edit_artist_modal
.metadata_profile_list
.set_items(lidarr_data.sorted_metadata_profile_names());
let metadata_profile_name = lidarr_data
.metadata_profile_map
.get_by_left(&artist.metadata_profile_id)
.unwrap();
let metadata_profile_index = edit_artist_modal
.metadata_profile_list
.items
.iter()
.position(|profile| profile == metadata_profile_name);
edit_artist_modal
.metadata_profile_list
.state
.select(metadata_profile_index);
edit_artist_modal
}
}
@@ -0,0 +1,48 @@
#[cfg(test)]
mod tests {
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::models::lidarr_models::{Artist, NewItemMonitorType};
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
#[test]
fn test_edit_artist_modal_from_lidarr_data() {
let mut lidarr_data = LidarrData {
quality_profile_map: BiMap::from_iter([(1i64, "HD - 1080p".to_owned()), (2i64, "Any".to_owned())]),
metadata_profile_map: BiMap::from_iter([(1i64, "Standard".to_owned()), (2i64, "None".to_owned())]),
tags_map: BiMap::from_iter([(1i64, "usenet".to_owned())]),
..LidarrData::default()
};
let artist = Artist {
id: 1,
monitored: true,
monitor_new_items: NewItemMonitorType::All,
quality_profile_id: 1,
metadata_profile_id: 1,
path: "/nfs/music/test_artist".to_owned(),
tags: vec![serde_json::Number::from(1)],
..Artist::default()
};
lidarr_data.artists.set_items(vec![artist]);
let edit_artist_modal = EditArtistModal::from(&lidarr_data);
assert_eq!(edit_artist_modal.monitored, Some(true));
assert_eq!(
*edit_artist_modal.monitor_list.current_selection(),
NewItemMonitorType::All
);
assert_str_eq!(
edit_artist_modal.quality_profile_list.current_selection(),
"HD - 1080p"
);
assert_str_eq!(
edit_artist_modal.metadata_profile_list.current_selection(),
"Standard"
);
assert_str_eq!(edit_artist_modal.path.text, "/nfs/music/test_artist");
assert_str_eq!(edit_artist_modal.tags.text, "usenet");
}
}
@@ -1,9 +1,10 @@
#[cfg(test)]
mod tests {
mod radarr_data_tests {
use bimap::BiMap;
use chrono::{DateTime, Utc};
use pretty_assertions::{assert_eq, assert_str_eq};
use serde_json::Number;
use crate::app::context_clues::{
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
@@ -61,6 +62,35 @@ mod tests {
assert_movie_info_tabs_reset!(radarr_data);
}
#[test]
fn test_tag_ids_to_display() {
let mut tags_map = BiMap::new();
tags_map.insert(3, "test 3".to_owned());
tags_map.insert(2, "test 2".to_owned());
tags_map.insert(1, "test 1".to_owned());
let radarr_data = RadarrData {
tags_map,
..RadarrData::default()
};
assert_str_eq!(radarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), "test 1, test 2");
}
#[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(2, "test 2".to_owned());
quality_profile_map.insert(1, "test 1".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()];
assert_iter_eq!(radarr_data.sorted_quality_profile_names(), expected_quality_profile_vec);
}
#[test]
fn test_radarr_data_defaults() {
let radarr_data = RadarrData::default();
@@ -1,9 +1,10 @@
#[cfg(test)]
mod tests {
mod sonarr_data_tests {
use bimap::BiMap;
use chrono::{DateTime, Utc};
use pretty_assertions::{assert_eq, assert_str_eq};
use serde_json::Number;
use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES;
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
use crate::models::stateful_table::StatefulTable;
@@ -77,6 +78,50 @@ mod tests {
assert_eq!(sonarr_data.series_info_tabs.index, 0);
}
#[test]
fn test_tag_ids_to_display() {
let mut tags_map = BiMap::new();
tags_map.insert(3, "test 3".to_owned());
tags_map.insert(2, "test 2".to_owned());
tags_map.insert(1, "test 1".to_owned());
let sonarr_data = SonarrData {
tags_map,
..SonarrData::default()
};
assert_str_eq!(sonarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), "test 1, test 2");
}
#[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(2, "test 2".to_owned());
quality_profile_map.insert(1, "test 1".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()];
assert_iter_eq!(sonarr_data.sorted_quality_profile_names(), expected_quality_profile_vec);
}
#[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(2, "test 2".to_owned());
language_profiles_map.insert(1, "test 1".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()];
assert_iter_eq!(sonarr_data.sorted_language_profile_names(), expected_language_profiles_vec);
}
#[test]
fn test_sonarr_data_defaults() {
let sonarr_data = SonarrData::default();
+116 -1
View File
@@ -3,7 +3,7 @@ use log::{debug, info, warn};
use serde_json::{Value, json};
use crate::models::Route;
use crate::models::lidarr_models::{Artist, DeleteArtistParams};
use crate::models::lidarr_models::{Artist, DeleteArtistParams, EditArtistParams};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_models::CommandBody;
use crate::network::lidarr_network::LidarrEvent;
@@ -168,4 +168,119 @@ impl Network<'_, '_> {
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn edit_artist(
&mut self,
mut edit_artist_params: EditArtistParams,
) -> Result<()> {
info!("Editing Lidarr artist");
if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() {
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
edit_artist_params.tags = Some(tag_ids_vec);
}
let artist_id = edit_artist_params.artist_id;
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
let event = LidarrEvent::EditArtist(EditArtistParams::default());
info!("Fetching artist details for artist with ID: {artist_id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{artist_id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
response = detailed_artist_body.to_string()
})
.await?;
info!("Constructing edit artist body");
let mut detailed_artist_body: Value = serde_json::from_str(&response)?;
let (
monitored,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags,
) = {
let monitored = edit_artist_params.monitored.unwrap_or(
detailed_artist_body["monitored"]
.as_bool()
.expect("Unable to deserialize 'monitored'"),
);
let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| {
serde_json::from_value(detailed_artist_body["monitorNewItems"].clone())
.expect("Unable to deserialize 'monitorNewItems'")
});
let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| {
detailed_artist_body["qualityProfileId"]
.as_i64()
.expect("Unable to deserialize 'qualityProfileId'")
});
let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| {
detailed_artist_body["metadataProfileId"]
.as_i64()
.expect("Unable to deserialize 'metadataProfileId'")
});
let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| {
detailed_artist_body["path"]
.as_str()
.expect("Unable to deserialize 'path'")
.to_owned()
});
let tags = if edit_artist_params.clear_tags {
vec![]
} else {
edit_artist_params.tags.unwrap_or(
detailed_artist_body["tags"]
.as_array()
.expect("Unable to deserialize 'tags'")
.iter()
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
.collect(),
)
};
(
monitored,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags,
)
};
*detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored);
*detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items);
*detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
*detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id);
*detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path);
*detailed_artist_body.get_mut("tags").unwrap() = json!(tags);
debug!("Edit artist body: {detailed_artist_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_artist_body),
Some(format!("/{artist_id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
}
@@ -0,0 +1,132 @@
#[cfg(test)]
#[allow(dead_code)] // TODO: maybe remove?
pub mod test_utils {
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus, DownloadRecord, DownloadStatus, DownloadsResponse, Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus};
use crate::models::servarr_models::{QualityProfile, RootFolder, Tag};
use crate::models::HorizontallyScrollableText;
use bimap::BiMap;
use chrono::DateTime;
use serde_json::Number;
pub fn member() -> Member {
Member {
name: Some("alex".to_owned()),
instrument: Some("piano".to_owned())
}
}
pub fn ratings() -> Ratings {
Ratings {
votes: 15,
value: 8.4
}
}
pub fn artist_statistics() -> ArtistStatistics {
ArtistStatistics {
album_count: 1,
track_file_count: 15,
track_count: 15,
total_track_count: 15,
size_on_disk: 12345,
percent_of_tracks: 99.9
}
}
pub fn artist() -> Artist {
Artist {
id: 1,
artist_name: "Alex".into(),
foreign_artist_id: "test-foreign-id".to_owned(),
status: ArtistStatus::Continuing,
overview: Some("some interesting description of the artist".to_owned()),
artist_type: Some("Person".to_owned()),
disambiguation: Some("American pianist".to_owned()),
members: Some(vec![member()]),
path: "/nfs/music/test-artist".to_owned(),
quality_profile_id: quality_profile().id,
metadata_profile_id: metadata_profile().id,
monitored: true,
monitor_new_items: NewItemMonitorType::All,
genres: vec!["soundtrack".to_owned()],
tags: vec![Number::from(tag().id)],
added: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
ratings: Some(ratings()),
statistics: Some(artist_statistics())
}
}
pub fn quality_profile() -> QualityProfile {
QualityProfile {
id: 1,
name: "Lossless".to_owned()
}
}
pub fn quality_profile_map() -> BiMap<i64, String> {
let quality_profile = quality_profile();
BiMap::from_iter(vec![(quality_profile.id, quality_profile.name)])
}
pub fn metadata_profile() -> MetadataProfile {
MetadataProfile {
id: 1,
name: "Standard".to_owned()
}
}
pub fn metadata_profile_map() -> BiMap<i64, String> {
let metadata_profile = metadata_profile();
BiMap::from_iter(vec![(metadata_profile.id, metadata_profile.name)])
}
pub fn tag() -> Tag {
Tag {
id: 1,
label: "alex".to_owned()
}
}
pub fn tags_map() -> BiMap<i64, String> {
let tag = tag();
BiMap::from_iter(vec![(tag.id, tag.label)])
}
pub fn download_record() -> DownloadRecord {
DownloadRecord {
title: "Test download title".to_owned(),
status: DownloadStatus::Downloading,
id: 1,
album_id: Some(Number::from(1i64)),
artist_id: Some(Number::from(1i64)),
size: 3543348019f64,
sizeleft: 1771674009f64,
output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")),
indexer: "kickass torrents".to_owned(),
download_client: Some("transmission".to_owned())
}
}
pub fn downloads_response() -> DownloadsResponse {
DownloadsResponse {
records: vec![download_record()]
}
}
pub fn system_status() -> SystemStatus {
SystemStatus {
version: "1.0".to_owned(),
start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
}
}
pub fn root_folder() -> RootFolder {
RootFolder {
id: 1,
path: "/nfs".to_owned(),
accessible: true,
free_space: 219902325555200,
unmapped_folders: None,
}
}
}
+70 -2
View File
@@ -2,7 +2,9 @@ use anyhow::Result;
use log::info;
use super::{NetworkEvent, NetworkResource};
use crate::models::lidarr_models::{DeleteArtistParams, LidarrSerdeable, MetadataProfile};
use crate::models::lidarr_models::{
DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
};
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
@@ -15,9 +17,15 @@ mod system;
#[path = "lidarr_network_tests.rs"]
mod lidarr_network_tests;
#[cfg(test)]
#[path = "lidarr_network_test_utils.rs"]
pub mod lidarr_network_test_utils;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum LidarrEvent {
AddTag(String),
DeleteArtist(DeleteArtistParams),
EditArtist(EditArtistParams),
GetArtistDetails(i64),
GetDiskSpace,
GetDownloads(u64),
@@ -37,7 +45,9 @@ pub enum LidarrEvent {
impl NetworkResource for LidarrEvent {
fn resource(&self) -> &'static str {
match &self {
LidarrEvent::AddTag(_) | LidarrEvent::GetTags => "/tag",
LidarrEvent::DeleteArtist(_)
| LidarrEvent::EditArtist(_)
| LidarrEvent::GetArtistDetails(_)
| LidarrEvent::ListArtists
| LidarrEvent::ToggleArtistMonitoring(_) => "/artist",
@@ -49,7 +59,6 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::GetQualityProfiles => "/qualityprofile",
LidarrEvent::GetRootFolders => "/rootfolder",
LidarrEvent::GetStatus => "/system/status",
LidarrEvent::GetTags => "/tag",
LidarrEvent::HealthCheck => "/health",
}
}
@@ -67,6 +76,7 @@ impl Network<'_, '_> {
lidarr_event: LidarrEvent,
) -> Result<LidarrSerdeable> {
match lidarr_event {
LidarrEvent::AddTag(tag) => self.add_lidarr_tag(tag).await.map(LidarrSerdeable::from),
LidarrEvent::DeleteArtist(params) => {
self.delete_artist(params).await.map(LidarrSerdeable::from)
}
@@ -111,6 +121,7 @@ impl Network<'_, '_> {
.await
.map(LidarrSerdeable::from),
LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from),
LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from),
}
}
@@ -180,4 +191,61 @@ impl Network<'_, '_> {
})
.await
}
async fn add_lidarr_tag(&mut self, tag: String) -> Result<Tag> {
info!("Adding a new Lidarr tag");
let event = LidarrEvent::AddTag(String::new());
let request_props = self
.request_props_from(
event,
RequestMethod::Post,
Some(serde_json::json!({ "label": tag })),
None,
None,
)
.await;
self
.handle_request::<serde_json::Value, Tag>(request_props, |tag, mut app| {
app.data.lidarr_data.tags_map.insert(tag.id, tag.label);
})
.await
}
pub(in crate::network::lidarr_network) async fn extract_and_add_lidarr_tag_ids_vec(
&mut self,
edit_tags: &str,
) -> Vec<i64> {
let missing_tags_vec = {
let tags_map = &self.app.lock().await.data.lidarr_data.tags_map;
edit_tags
.split(',')
.filter(|&tag| {
!tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none()
})
.collect::<Vec<&str>>()
};
for tag in missing_tags_vec {
self
.add_lidarr_tag(tag.trim().to_owned())
.await
.expect("Unable to add tag");
}
let app = self.app.lock().await;
edit_tags
.split(',')
.filter(|tag| !tag.is_empty())
.map(|tag| {
*app
.data
.lidarr_data
.tags_map
.get_by_right(tag.to_lowercase().trim())
.unwrap()
})
.collect()
}
}
@@ -284,6 +284,7 @@ pub mod test_utils {
subtitles: Some("English".to_owned()),
}
}
pub fn quality() -> Quality {
Quality {
name: "Bluray-1080p".to_owned(),
+222
View File
@@ -0,0 +1,222 @@
use std::sync::atomic::Ordering;
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::Layout;
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::modals::EditArtistModal;
use crate::render_selectable_input_box;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{title_block_centered};
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{DrawUi, draw_popup};
#[cfg(test)]
#[path = "edit_artist_ui_tests.rs"]
mod edit_artist_ui_tests;
pub(super) struct EditArtistUi;
impl DrawUi for EditArtistUi {
fn accepts(route: Route) -> bool {
let Route::Lidarr(active_lidarr_block, _) = route else {
return false;
};
EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
if let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() {
let draw_edit_artist_prompt = |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| {
draw_edit_artist_confirmation_prompt(f, app, prompt_area);
match active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => {
draw_edit_artist_select_monitor_new_items_popup(f, app);
}
ActiveLidarrBlock::EditArtistSelectQualityProfile => {
draw_edit_artist_select_quality_profile_popup(f, app);
}
ActiveLidarrBlock::EditArtistSelectMetadataProfile => {
draw_edit_artist_select_metadata_profile_popup(f, app);
}
_ => (),
}
};
draw_popup(f, app, draw_edit_artist_prompt, Size::Long);
}
}
}
fn draw_edit_artist_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let artist_name = app
.data
.lidarr_data
.artists
.current_selection()
.artist_name
.text
.clone();
let title = format!("Edit - {artist_name}");
f.render_widget(title_block_centered(&title), area);
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::EditArtistConfirmPrompt;
let EditArtistModal {
monitor_list,
quality_profile_list,
metadata_profile_list,
monitored,
path,
tags,
} = app
.data
.lidarr_data
.edit_artist_modal
.as_ref()
.expect("edit_artist_modal must exist in this context");
let selected_monitor_new_items = monitor_list.current_selection();
let selected_quality_profile = quality_profile_list.current_selection();
let selected_metadata_profile = metadata_profile_list.current_selection();
let [
_,
monitored_area,
monitor_new_items_area,
quality_profile_area,
metadata_profile_area,
path_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 [save_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area);
let monitored_checkbox = Checkbox::new("Monitored")
.checked(monitored.unwrap_or_default())
.highlighted(selected_block == ActiveLidarrBlock::EditArtistToggleMonitored);
let monitor_new_items_drop_down_button = Button::new()
.title(selected_monitor_new_items.to_display_str())
.label("Monitor New Albums")
.icon("")
.selected(selected_block == ActiveLidarrBlock::EditArtistSelectMonitorNewItems);
let quality_profile_drop_down_button = Button::new()
.title(selected_quality_profile)
.label("Quality Profile")
.icon("")
.selected(selected_block == ActiveLidarrBlock::EditArtistSelectQualityProfile);
let metadata_profile_drop_down_button = Button::new()
.title(selected_metadata_profile)
.label("Metadata Profile")
.icon("")
.selected(selected_block == ActiveLidarrBlock::EditArtistSelectMetadataProfile);
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let path_input_box = InputBox::new(&path.text)
.offset(path.offset.load(Ordering::SeqCst))
.label("Path")
.highlighted(selected_block == ActiveLidarrBlock::EditArtistPathInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditArtistPathInput);
let tags_input_box = InputBox::new(&tags.text)
.offset(tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == ActiveLidarrBlock::EditArtistTagsInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditArtistTagsInput);
match active_lidarr_block {
ActiveLidarrBlock::EditArtistPathInput => path_input_box.show_cursor(f, path_area),
ActiveLidarrBlock::EditArtistTagsInput => tags_input_box.show_cursor(f, tags_area),
_ => (),
}
render_selectable_input_box!(path_input_box, f, path_area);
render_selectable_input_box!(tags_input_box, f, tags_area);
}
let save_button = Button::new()
.title("Save")
.selected(yes_no_value && highlight_yes_no);
let cancel_button = Button::new()
.title("Cancel")
.selected(!yes_no_value && highlight_yes_no);
f.render_widget(monitored_checkbox, monitored_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);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
}
fn draw_edit_artist_select_monitor_new_items_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let monitor_list = SelectableList::new(
&mut app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.expect("edit_artist_modal must exist in this context")
.monitor_list,
|monitor_type| ListItem::new(monitor_type.to_display_str().to_owned()),
);
let popup = Popup::new(monitor_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
fn draw_edit_artist_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let quality_profile_list = SelectableList::new(
&mut app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.expect("edit_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_edit_artist_select_metadata_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let metadata_profile_list = SelectableList::new(
&mut app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.expect("edit_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());
}
@@ -0,0 +1,22 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use strum::IntoEnumIterator;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::edit_artist_ui::EditArtistUi;
#[test]
fn test_edit_artist_ui_accepts() {
let mut edit_artist_ui_blocks = Vec::new();
for block in ActiveLidarrBlock::iter() {
if EditArtistUi::accepts(Route::Lidarr(block, None)) {
edit_artist_ui_blocks.push(block);
}
}
assert_eq!(edit_artist_ui_blocks, EDIT_ARTIST_BLOCKS.to_vec());
}
}
+3 -1
View File
@@ -4,7 +4,7 @@ mod tests {
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, LIBRARY_BLOCKS,
ActiveLidarrBlock, 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,6 +17,8 @@ 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(EDIT_ARTIST_BLOCKS);
for active_lidarr_block in ActiveLidarrBlock::iter() {
if library_ui_blocks.contains(&active_lidarr_block) {
assert!(LibraryUi::accepts(active_lidarr_block.into()));
+6 -1
View File
@@ -1,4 +1,5 @@
use delete_artist_ui::DeleteArtistUi;
use edit_artist_ui::EditArtistUi;
use ratatui::{
Frame,
layout::{Constraint, Rect},
@@ -26,6 +27,7 @@ use crate::{
};
mod delete_artist_ui;
mod edit_artist_ui;
#[cfg(test)]
#[path = "library_ui_tests.rs"]
@@ -36,7 +38,9 @@ pub(super) struct LibraryUi;
impl DrawUi for LibraryUi {
fn accepts(route: Route) -> bool {
if let Route::Lidarr(active_lidarr_block, _) = route {
return DeleteArtistUi::accepts(route) || LIBRARY_BLOCKS.contains(&active_lidarr_block);
return DeleteArtistUi::accepts(route)
|| EditArtistUi::accepts(route)
|| LIBRARY_BLOCKS.contains(&active_lidarr_block);
}
false
@@ -48,6 +52,7 @@ impl DrawUi for LibraryUi {
match route {
_ if DeleteArtistUi::accepts(route) => DeleteArtistUi::draw(f, app, area),
_ if EditArtistUi::accepts(route) => EditArtistUi::draw(f, app, area),
Route::Lidarr(ActiveLidarrBlock::UpdateAllArtistsPrompt, _) => {
let confirmation_prompt = ConfirmationPrompt::new()
.title("Update All Artists")