feat: Full support for deleting an artist via CLI and TUI
This commit is contained in:
@@ -1,11 +1,24 @@
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{ContextClue, ContextClueProvider};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_context_clues_tests.rs"]
|
||||
mod lidarr_context_clues_tests;
|
||||
|
||||
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 6] = [
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
pub(in crate::app) struct LidarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for LidarrContextClueProvider {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
mod tests {
|
||||
use crate::app::context_clues::ContextClueProvider;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
|
||||
use crate::app::lidarr::lidarr_context_clues::{LidarrContextClueProvider, ARTISTS_CONTEXT_CLUES};
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, ARTISTS_CONTEXT_CLUES,
|
||||
ActiveLidarrBlock,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
|
||||
@@ -17,6 +17,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.delete, DEFAULT_KEYBINDINGS.delete.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::network::NetworkEvent;
|
||||
@@ -14,6 +15,7 @@ mod tests {
|
||||
|
||||
app.dispatch_by_lidarr_block(&ActiveLidarrBlock::Artists).await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetQualityProfiles.into()
|
||||
@@ -24,5 +26,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into());
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
|
||||
use super::App;
|
||||
|
||||
pub(in crate::app) mod lidarr_context_clues;
|
||||
pub mod lidarr_context_clues;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_tests.rs"]
|
||||
|
||||
+1
-1
@@ -26,9 +26,9 @@ mod app_tests;
|
||||
pub mod context_clues;
|
||||
pub mod key_binding;
|
||||
mod key_binding_tests;
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
pub mod lidarr;
|
||||
|
||||
pub struct App<'a> {
|
||||
navigation_stack: Vec<Route>,
|
||||
|
||||
@@ -455,7 +455,6 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
// This should panic because the route is not a Sonarr route
|
||||
SonarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
|
||||
+13
-4
@@ -2,18 +2,16 @@
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::{CommandFactory, error::ErrorKind};
|
||||
use clap::{error::ErrorKind, CommandFactory};
|
||||
use mockall::predicate::eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
Cli,
|
||||
app::App,
|
||||
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand},
|
||||
models::{
|
||||
Serdeable,
|
||||
radarr_models::{
|
||||
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
|
||||
RadarrSerdeable,
|
||||
@@ -22,10 +20,12 @@ mod tests {
|
||||
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
|
||||
SonarrSerdeable,
|
||||
},
|
||||
Serdeable,
|
||||
},
|
||||
network::{
|
||||
MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent, sonarr_network::SonarrEvent,
|
||||
radarr_network::RadarrEvent, sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent,
|
||||
},
|
||||
Cli,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -55,6 +55,13 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_subcommand_delegates_to_lidarr() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completions_requires_argument() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||
@@ -174,4 +181,6 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
// TODO: Implement test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::lidarr_models::DeleteArtistParams,
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_command_handler_tests.rs"]
|
||||
mod delete_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrDeleteCommand {
|
||||
#[command(about = "Delete an artist from your Lidarr library")]
|
||||
Artist {
|
||||
#[arg(long, help = "The ID of the artist to delete", required = true)]
|
||||
artist_id: i64,
|
||||
#[arg(long, help = "Delete the artist files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this artist")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrDeleteCommand> for Command {
|
||||
fn from(value: LidarrDeleteCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Delete(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrDeleteCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrDeleteCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrDeleteCommand::Artist {
|
||||
artist_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_artist_params = DeleteArtistParams {
|
||||
id: artist_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteArtist(delete_artist_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, delete_command_handler::LidarrDeleteCommand},
|
||||
},
|
||||
};
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_delete_command_from() {
|
||||
let command = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Delete(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_defaults() {
|
||||
let expected_args = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "lidarr", "delete", "artist", "--artist-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_all_args_defined() {
|
||||
let expected_args = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
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::delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler},
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{DeleteArtistParams, LidarrSerdeable},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_artist_command() {
|
||||
let expected_delete_artist_params = DeleteArtistParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteArtist(expected_delete_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 delete_artist_command = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,5 +33,92 @@ mod tests {
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_delete_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
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::{
|
||||
LidarrCliHandler, LidarrCommand,
|
||||
delete_command_handler::LidarrDeleteCommand,
|
||||
list_command_handler::LidarrListCommand,
|
||||
},
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{Artist, DeleteArtistParams, LidarrSerdeable},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
|
||||
let expected_delete_artist_params = DeleteArtistParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteArtist(expected_delete_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 delete_artist_command = LidarrCommand::Delete(LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, delete_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::ListArtists.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Artists(vec![
|
||||
Artist::default(),
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_artists_command = LidarrCommand::List(LidarrListCommand::Artists);
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, list_artists_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -2,16 +2,15 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
|
||||
use list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
network::NetworkTrait,
|
||||
};
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
use super::{CliCommandHandler, Command};
|
||||
|
||||
mod delete_command_handler;
|
||||
mod list_command_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -20,6 +19,11 @@ mod lidarr_command_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrCommand {
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to delete resources from your Lidarr instance"
|
||||
)]
|
||||
Delete(LidarrDeleteCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to list attributes from your Lidarr instance"
|
||||
@@ -54,6 +58,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrCommand::Delete(delete_command) => {
|
||||
LidarrDeleteCommandHandler::with(self.app, delete_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::List(list_command) => {
|
||||
LidarrListCommandHandler::with(self.app, list_command, self.network)
|
||||
.handle()
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::models::lidarr_models::DeleteArtistParams;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
event::Key,
|
||||
handlers::{KeyEventHandler, handle_prompt_toggle},
|
||||
matches_key,
|
||||
models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_artist_handler_tests.rs"]
|
||||
mod delete_artist_handler_tests;
|
||||
|
||||
pub(in crate::handlers::lidarr_handlers) struct DeleteArtistHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl DeleteArtistHandler<'_, '_> {
|
||||
fn build_delete_artist_params(&mut self) -> DeleteArtistParams {
|
||||
let id = self.app.data.lidarr_data.artists.current_selection().id;
|
||||
let delete_files = self.app.data.lidarr_data.delete_artist_files;
|
||||
let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion;
|
||||
self.app.data.lidarr_data.reset_delete_artist_preferences();
|
||||
|
||||
DeleteArtistParams {
|
||||
id,
|
||||
delete_files,
|
||||
add_import_list_exclusion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
DELETE_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>,
|
||||
) -> Self {
|
||||
DeleteArtistHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
self.app.data.lidarr_data.selected_block.up();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
self.app.data.lidarr_data.selected_block.down();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
handle_prompt_toggle(self.app, self.key);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
match self.app.data.lidarr_data.selected_block.get_active_block() {
|
||||
ActiveLidarrBlock::DeleteArtistConfirmPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params()));
|
||||
self.app.should_refresh = true;
|
||||
} else {
|
||||
self.app.data.lidarr_data.reset_delete_artist_preferences();
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::DeleteArtistToggleDeleteFile => {
|
||||
self.app.data.lidarr_data.delete_artist_files =
|
||||
!self.app.data.lidarr_data.delete_artist_files;
|
||||
}
|
||||
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion => {
|
||||
self.app.data.lidarr_data.add_import_list_exclusion =
|
||||
!self.app.data.lidarr_data.add_import_list_exclusion;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.reset_delete_artist_preferences();
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt
|
||||
&& self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::DeleteArtistConfirmPrompt
|
||||
&& matches_key!(confirm, self.key)
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::DeleteArtist(self.build_delete_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) -> crate::models::Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::delete_artist_handler::DeleteArtistHandler;
|
||||
use crate::models::lidarr_models::{Artist, DeleteArtistParams};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS};
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app.data.lidarr_data.selected_block.down();
|
||||
|
||||
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
|
||||
|
||||
if key == Key::Up {
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteArtistToggleDeleteFile
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteArtistConfirmPrompt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_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.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app.data.lidarr_data.selected_block.down();
|
||||
|
||||
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
|
||||
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_prompt_prompt_decline_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.lidarr_data.delete_artist_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_artist_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_confirm_prompt_prompt_confirmation_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_artist_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
let expected_delete_artist_params = DeleteArtistParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::DeleteArtist(expected_delete_artist_params))
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_artist_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_artist_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::DeleteArtistPrompt.into()
|
||||
);
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert!(!app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(app.data.lidarr_data.delete_artist_files);
|
||||
assert!(app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_toggle_delete_files_submit() {
|
||||
let current_route = ActiveLidarrBlock::DeleteArtistPrompt.into();
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), current_route);
|
||||
assert_eq!(app.data.lidarr_data.delete_artist_files, true);
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), current_route);
|
||||
assert_eq!(app.data.lidarr_data.delete_artist_files, false);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
use rstest::rstest;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_artist_prompt_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_artist_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_artist_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use crate::{
|
||||
assert_navigation_popped,
|
||||
models::{
|
||||
BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS,
|
||||
},
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_confirm_prompt_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.delete_artist_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
let expected_delete_artist_params = DeleteArtistParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::DeleteArtist(expected_delete_artist_params))
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_artist_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_handler_accepts() {
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(DeleteArtistHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!DeleteArtistHandler::accepts(active_lidarr_block));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_artist_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 = DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_delete_artist_params() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.data.lidarr_data.delete_artist_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
let expected_delete_artist_params = DeleteArtistParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
|
||||
let delete_artist_params = DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.build_delete_artist_params();
|
||||
|
||||
assert_eq!(delete_artist_params, expected_delete_artist_params);
|
||||
assert!(!app.data.lidarr_data.delete_artist_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_handler_ready_when_not_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,11 @@ use crate::{
|
||||
handlers::{KeyEventHandler, handle_clear_errors},
|
||||
matches_key,
|
||||
models::{
|
||||
BlockSelectionState,
|
||||
lidarr_models::Artist,
|
||||
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS},
|
||||
servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
|
||||
},
|
||||
stateful_table::SortOption,
|
||||
},
|
||||
};
|
||||
@@ -13,6 +16,10 @@ use crate::{
|
||||
use super::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
|
||||
mod delete_artist_handler;
|
||||
|
||||
pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "library_handler_tests.rs"]
|
||||
mod library_handler_tests;
|
||||
@@ -84,7 +91,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
fn handle_delete(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::Artists {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::Artists {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use library::LibraryHandler;
|
||||
use library::{DeleteArtistHandler, LibraryHandler};
|
||||
|
||||
use crate::{
|
||||
app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
|
||||
@@ -22,6 +22,10 @@ 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();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use derivative::Derivative;
|
||||
use enum_display_style_derive::EnumDisplayStyle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value};
|
||||
use strum::EnumIter;
|
||||
use strum::{Display, EnumIter};
|
||||
|
||||
use super::{HorizontallyScrollableText, Serdeable};
|
||||
use crate::serde_enum_from;
|
||||
@@ -45,7 +45,7 @@ pub struct Artist {
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
strum::Display,
|
||||
Display,
|
||||
EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -134,7 +134,7 @@ impl Eq for DownloadRecord {}
|
||||
Copy,
|
||||
Debug,
|
||||
EnumIter,
|
||||
strum::Display,
|
||||
Display,
|
||||
EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -167,6 +167,14 @@ pub struct SystemStatus {
|
||||
pub start_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct DeleteArtistParams {
|
||||
pub id: i64,
|
||||
pub delete_files: bool,
|
||||
pub add_import_list_exclusion: bool,
|
||||
}
|
||||
|
||||
impl From<LidarrSerdeable> for Serdeable {
|
||||
fn from(value: LidarrSerdeable) -> Serdeable {
|
||||
Serdeable::Lidarr(value)
|
||||
|
||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
||||
use strum::EnumIter;
|
||||
#[cfg(test)]
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
|
||||
use crate::models::{
|
||||
Route, TabRoute, TabState,
|
||||
lidarr_models::{Artist, DownloadRecord},
|
||||
@@ -17,7 +17,9 @@ use crate::network::lidarr_network::LidarrEvent;
|
||||
mod lidarr_data_tests;
|
||||
|
||||
pub struct LidarrData<'a> {
|
||||
pub add_import_list_exclusion: bool,
|
||||
pub artists: StatefulTable<Artist>,
|
||||
pub delete_artist_files: bool,
|
||||
pub disk_space_vec: Vec<DiskSpace>,
|
||||
pub downloads: StatefulTable<DownloadRecord>,
|
||||
pub main_tabs: TabState,
|
||||
@@ -33,15 +35,18 @@ pub struct LidarrData<'a> {
|
||||
}
|
||||
|
||||
impl LidarrData<'_> {
|
||||
pub fn reset_sorting(&mut self) {
|
||||
self.artists.sorting(vec![]);
|
||||
pub fn reset_delete_artist_preferences(&mut self) {
|
||||
self.delete_artist_files = false;
|
||||
self.add_import_list_exclusion = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Default for LidarrData<'a> {
|
||||
fn default() -> LidarrData<'a> {
|
||||
LidarrData {
|
||||
add_import_list_exclusion: false,
|
||||
artists: StatefulTable::default(),
|
||||
delete_artist_files: false,
|
||||
disk_space_vec: Vec::new(),
|
||||
downloads: StatefulTable::default(),
|
||||
metadata_profile_map: BiMap::new(),
|
||||
@@ -78,6 +83,8 @@ impl LidarrData<'_> {
|
||||
name: "Name",
|
||||
cmp_fn: Some(|a: &Artist, b: &Artist| a.artist_name.text.cmp(&b.artist_name.text)),
|
||||
}]);
|
||||
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())]);
|
||||
@@ -93,26 +100,16 @@ impl LidarrData<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::app::context_clues::ContextClue;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
|
||||
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)]
|
||||
#[cfg_attr(test, derive(Display, EnumString))]
|
||||
pub enum ActiveLidarrBlock {
|
||||
#[default]
|
||||
Artists,
|
||||
ArtistsSortPrompt,
|
||||
DeleteArtistPrompt,
|
||||
DeleteArtistConfirmPrompt,
|
||||
DeleteArtistToggleDeleteFile,
|
||||
DeleteArtistToggleAddListExclusion,
|
||||
SearchArtists,
|
||||
SearchArtistsError,
|
||||
FilterArtists,
|
||||
@@ -128,6 +125,19 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 6] = [
|
||||
ActiveLidarrBlock::FilterArtistsError,
|
||||
];
|
||||
|
||||
pub static DELETE_ARTIST_BLOCKS: [ActiveLidarrBlock; 4] = [
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistConfirmPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistToggleDeleteFile,
|
||||
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion,
|
||||
];
|
||||
|
||||
pub const DELETE_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
|
||||
&[ActiveLidarrBlock::DeleteArtistToggleDeleteFile],
|
||||
&[ActiveLidarrBlock::DeleteArtistToggleAddListExclusion],
|
||||
&[ActiveLidarrBlock::DeleteArtistConfirmPrompt],
|
||||
];
|
||||
|
||||
impl From<ActiveLidarrBlock> for Route {
|
||||
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
|
||||
Route::Lidarr(active_lidarr_block, None)
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::models::{servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, Route};
|
||||
use crate::models::{
|
||||
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData},
|
||||
Route,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_from_active_lidarr_block_to_route() {
|
||||
@@ -19,4 +22,18 @@ mod tests {
|
||||
Route::Lidarr(ActiveLidarrBlock::Artists, Some(ActiveLidarrBlock::Artists),)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_delete_artist_preferences() {
|
||||
let mut lidarr_data = LidarrData{
|
||||
delete_artist_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
..LidarrData::default()
|
||||
};
|
||||
|
||||
lidarr_data.reset_delete_artist_preferences();
|
||||
|
||||
assert!(!lidarr_data.delete_artist_files);
|
||||
assert!(!lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::models::Route;
|
||||
|
||||
pub mod lidarr;
|
||||
pub mod modals;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
pub mod lidarr;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(in crate::models::servarr_data) mod data_test_utils;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::{DownloadsResponse, LidarrSerdeable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_downloads_event() {
|
||||
let downloads_json = json!({
|
||||
"records": [{
|
||||
"title": "Test Album",
|
||||
"status": "downloading",
|
||||
"id": 1,
|
||||
"size": 100.0,
|
||||
"sizeleft": 50.0,
|
||||
"indexer": "test-indexer"
|
||||
}]
|
||||
});
|
||||
let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(downloads_json)
|
||||
.query("pageSize=500")
|
||||
.build_for(LidarrEvent::GetDownloads(500))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetDownloads(500))
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else {
|
||||
panic!("Expected DownloadsResponse");
|
||||
};
|
||||
|
||||
assert_eq!(downloads_response, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.downloads.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use crate::models::lidarr_models::DownloadsResponse;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_downloads_network_tests.rs"]
|
||||
mod lidarr_downloads_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_downloads(
|
||||
&mut self,
|
||||
count: u64,
|
||||
) -> Result<DownloadsResponse> {
|
||||
info!("Fetching Lidarr downloads");
|
||||
let event = LidarrEvent::GetDownloads(count);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("pageSize={count}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| {
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.downloads
|
||||
.set_items(queue_response.records);
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::{Artist, LidarrSerdeable};
|
||||
use crate::models::lidarr_models::{Artist, DeleteArtistParams, LidarrSerdeable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -41,4 +41,29 @@ mod tests {
|
||||
assert_eq!(artists, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.artists.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_artist_event() {
|
||||
let delete_artist_params = DeleteArtistParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let (async_server, app, _server) = MockServarrApi::delete()
|
||||
.path("/1")
|
||||
.query("deleteFiles=true&addImportListExclusion=true")
|
||||
.build_for(LidarrEvent::DeleteArtist(delete_artist_params.clone()))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
assert!(
|
||||
network
|
||||
.handle_lidarr_event(LidarrEvent::DeleteArtist(delete_artist_params))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use crate::models::lidarr_models::Artist;
|
||||
use crate::models::lidarr_models::{Artist, DeleteArtistParams};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::Route;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
@@ -12,6 +12,38 @@ use crate::network::{Network, RequestMethod};
|
||||
mod lidarr_library_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn delete_artist(
|
||||
&mut self,
|
||||
delete_artist_params: DeleteArtistParams,
|
||||
) -> Result<()> {
|
||||
let event = LidarrEvent::DeleteArtist(DeleteArtistParams::default());
|
||||
let DeleteArtistParams {
|
||||
id,
|
||||
delete_files,
|
||||
add_import_list_exclusion,
|
||||
} = delete_artist_params;
|
||||
|
||||
info!(
|
||||
"Deleting Lidarr artist with ID: {id} with deleteFiles={delete_files} and addImportListExclusion={add_import_list_exclusion}"
|
||||
);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{id}")),
|
||||
Some(format!(
|
||||
"deleteFiles={delete_files}&addImportListExclusion={add_import_list_exclusion}"
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn list_artists(&mut self) -> Result<Vec<Artist>> {
|
||||
info!("Fetching Lidarr artists");
|
||||
let event = LidarrEvent::ListArtists;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent};
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use crate::models::lidarr_models::{LidarrSerdeable, MetadataProfile};
|
||||
use crate::models::servarr_models::{QualityProfile, Tag};
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use crate::network::{lidarr_network::LidarrEvent, NetworkEvent, NetworkResource};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
|
||||
#[rstest]
|
||||
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
|
||||
@@ -25,4 +29,109 @@ mod tests {
|
||||
NetworkEvent::from(LidarrEvent::HealthCheck)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_metadata_profiles_event() {
|
||||
let metadata_profiles_json = json!([{
|
||||
"id": 1,
|
||||
"name": "Standard"
|
||||
}]);
|
||||
let response: Vec<MetadataProfile> =
|
||||
serde_json::from_value(metadata_profiles_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(metadata_profiles_json)
|
||||
.build_for(LidarrEvent::GetMetadataProfiles)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetMetadataProfiles)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else {
|
||||
panic!("Expected MetadataProfiles");
|
||||
};
|
||||
|
||||
assert_eq!(metadata_profiles, response);
|
||||
assert_eq!(
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.metadata_profile_map
|
||||
.get_by_left(&1),
|
||||
Some(&"Standard".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_quality_profiles_event() {
|
||||
let quality_profiles_json = json!([{
|
||||
"id": 1,
|
||||
"name": "Lossless"
|
||||
}]);
|
||||
let response: Vec<QualityProfile> =
|
||||
serde_json::from_value(quality_profiles_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(quality_profiles_json)
|
||||
.build_for(LidarrEvent::GetQualityProfiles)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetQualityProfiles)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else {
|
||||
panic!("Expected QualityProfiles");
|
||||
};
|
||||
|
||||
assert_eq!(quality_profiles, response);
|
||||
assert_eq!(
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.quality_profile_map
|
||||
.get_by_left(&1),
|
||||
Some(&"Lossless".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_tags_event() {
|
||||
let tags_json = json!([{
|
||||
"id": 1,
|
||||
"label": "usenet"
|
||||
}]);
|
||||
let response: Vec<Tag> = serde_json::from_value(tags_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(tags_json)
|
||||
.build_for(LidarrEvent::GetTags)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::GetTags).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::Tags(tags) = result.unwrap() else {
|
||||
panic!("Expected Tags");
|
||||
};
|
||||
|
||||
assert_eq!(tags, response);
|
||||
assert_eq!(
|
||||
app.lock().await.data.lidarr_data.tags_map.get_by_left(&1),
|
||||
Some(&"usenet".to_owned())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use super::{NetworkEvent, NetworkResource};
|
||||
use crate::models::lidarr_models::LidarrSerdeable;
|
||||
use crate::network::Network;
|
||||
use crate::models::lidarr_models::{DeleteArtistParams, LidarrSerdeable, MetadataProfile};
|
||||
use crate::models::servarr_models::{QualityProfile, Tag};
|
||||
use crate::network::{Network, RequestMethod};
|
||||
|
||||
mod downloads;
|
||||
mod library;
|
||||
mod root_folders;
|
||||
mod system;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -13,6 +17,7 @@ mod lidarr_network_tests;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum LidarrEvent {
|
||||
DeleteArtist(DeleteArtistParams),
|
||||
GetDiskSpace,
|
||||
GetDownloads(u64),
|
||||
GetMetadataProfiles,
|
||||
@@ -27,6 +32,7 @@ pub enum LidarrEvent {
|
||||
impl NetworkResource for LidarrEvent {
|
||||
fn resource(&self) -> &'static str {
|
||||
match &self {
|
||||
LidarrEvent::DeleteArtist(_) | LidarrEvent::ListArtists => "/artist",
|
||||
LidarrEvent::GetDiskSpace => "/diskspace",
|
||||
LidarrEvent::GetDownloads(_) => "/queue",
|
||||
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
|
||||
@@ -35,7 +41,6 @@ impl NetworkResource for LidarrEvent {
|
||||
LidarrEvent::GetStatus => "/system/status",
|
||||
LidarrEvent::GetTags => "/tag",
|
||||
LidarrEvent::HealthCheck => "/health",
|
||||
LidarrEvent::ListArtists => "/artist",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +57,9 @@ impl Network<'_, '_> {
|
||||
lidarr_event: LidarrEvent,
|
||||
) -> Result<LidarrSerdeable> {
|
||||
match lidarr_event {
|
||||
LidarrEvent::DeleteArtist(params) => {
|
||||
self.delete_artist(params).await.map(LidarrSerdeable::from)
|
||||
}
|
||||
LidarrEvent::GetDiskSpace => self
|
||||
.get_lidarr_diskspace()
|
||||
.await
|
||||
@@ -84,4 +92,58 @@ impl Network<'_, '_> {
|
||||
LidarrEvent::ListArtists => self.list_artists().await.map(LidarrSerdeable::from),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_lidarr_metadata_profiles(&mut self) -> Result<Vec<MetadataProfile>> {
|
||||
info!("Fetching Lidarr metadata profiles");
|
||||
let event = LidarrEvent::GetMetadataProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<MetadataProfile>>(request_props, |metadata_profiles, mut app| {
|
||||
app.data.lidarr_data.metadata_profile_map = metadata_profiles
|
||||
.into_iter()
|
||||
.map(|profile| (profile.id, profile.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_lidarr_quality_profiles(&mut self) -> Result<Vec<QualityProfile>> {
|
||||
info!("Fetching Lidarr quality profiles");
|
||||
let event = LidarrEvent::GetQualityProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
|
||||
app.data.lidarr_data.quality_profile_map = quality_profiles
|
||||
.into_iter()
|
||||
.map(|profile| (profile.id, profile.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_lidarr_tags(&mut self) -> Result<Vec<Tag>> {
|
||||
info!("Fetching Lidarr tags");
|
||||
let event = LidarrEvent::GetTags;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
|
||||
app.data.lidarr_data.tags_map = tags_vec
|
||||
.into_iter()
|
||||
.map(|tag| (tag.id, tag.label))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::LidarrSerdeable;
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_root_folders_event() {
|
||||
let root_folders_json = json!([{
|
||||
"id": 1,
|
||||
"path": "/music",
|
||||
"accessible": true,
|
||||
"freeSpace": 50000000000i64
|
||||
}]);
|
||||
let response: Vec<RootFolder> = serde_json::from_value(root_folders_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(root_folders_json)
|
||||
.build_for(LidarrEvent::GetRootFolders)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetRootFolders)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else {
|
||||
panic!("Expected RootFolders");
|
||||
};
|
||||
|
||||
assert_eq!(root_folders, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_root_folders_network_tests.rs"]
|
||||
mod lidarr_root_folders_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders(
|
||||
&mut self,
|
||||
) -> Result<Vec<RootFolder>> {
|
||||
info!("Fetching Lidarr root folders");
|
||||
let event = LidarrEvent::GetRootFolders;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
|
||||
app.data.lidarr_data.root_folders.set_items(root_folders);
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::{
|
||||
DownloadsResponse, LidarrSerdeable, MetadataProfile, SystemStatus,
|
||||
};
|
||||
use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag};
|
||||
use crate::models::lidarr_models::{LidarrSerdeable, SystemStatus};
|
||||
use crate::models::servarr_models::DiskSpace;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -22,111 +20,6 @@ mod tests {
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_metadata_profiles_event() {
|
||||
let metadata_profiles_json = json!([{
|
||||
"id": 1,
|
||||
"name": "Standard"
|
||||
}]);
|
||||
let response: Vec<MetadataProfile> =
|
||||
serde_json::from_value(metadata_profiles_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(metadata_profiles_json)
|
||||
.build_for(LidarrEvent::GetMetadataProfiles)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetMetadataProfiles)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else {
|
||||
panic!("Expected MetadataProfiles");
|
||||
};
|
||||
|
||||
assert_eq!(metadata_profiles, response);
|
||||
assert_eq!(
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.metadata_profile_map
|
||||
.get_by_left(&1),
|
||||
Some(&"Standard".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_quality_profiles_event() {
|
||||
let quality_profiles_json = json!([{
|
||||
"id": 1,
|
||||
"name": "Lossless"
|
||||
}]);
|
||||
let response: Vec<QualityProfile> =
|
||||
serde_json::from_value(quality_profiles_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(quality_profiles_json)
|
||||
.build_for(LidarrEvent::GetQualityProfiles)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetQualityProfiles)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else {
|
||||
panic!("Expected QualityProfiles");
|
||||
};
|
||||
|
||||
assert_eq!(quality_profiles, response);
|
||||
assert_eq!(
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.quality_profile_map
|
||||
.get_by_left(&1),
|
||||
Some(&"Lossless".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_tags_event() {
|
||||
let tags_json = json!([{
|
||||
"id": 1,
|
||||
"label": "usenet"
|
||||
}]);
|
||||
let response: Vec<Tag> = serde_json::from_value(tags_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(tags_json)
|
||||
.build_for(LidarrEvent::GetTags)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::GetTags).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::Tags(tags) = result.unwrap() else {
|
||||
panic!("Expected Tags");
|
||||
};
|
||||
|
||||
assert_eq!(tags, response);
|
||||
assert_eq!(
|
||||
app.lock().await.data.lidarr_data.tags_map.get_by_left(&1),
|
||||
Some(&"usenet".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_diskspace_event() {
|
||||
let diskspace_json = json!([{
|
||||
@@ -153,71 +46,6 @@ mod tests {
|
||||
assert!(!app.lock().await.data.lidarr_data.disk_space_vec.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_downloads_event() {
|
||||
let downloads_json = json!({
|
||||
"records": [{
|
||||
"title": "Test Album",
|
||||
"status": "downloading",
|
||||
"id": 1,
|
||||
"size": 100.0,
|
||||
"sizeleft": 50.0,
|
||||
"indexer": "test-indexer"
|
||||
}]
|
||||
});
|
||||
let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(downloads_json)
|
||||
.query("pageSize=500")
|
||||
.build_for(LidarrEvent::GetDownloads(500))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetDownloads(500))
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else {
|
||||
panic!("Expected DownloadsResponse");
|
||||
};
|
||||
|
||||
assert_eq!(downloads_response, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.downloads.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_root_folders_event() {
|
||||
let root_folders_json = json!([{
|
||||
"id": 1,
|
||||
"path": "/music",
|
||||
"accessible": true,
|
||||
"freeSpace": 50000000000i64
|
||||
}]);
|
||||
let response: Vec<RootFolder> = serde_json::from_value(root_folders_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(root_folders_json)
|
||||
.build_for(LidarrEvent::GetRootFolders)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetRootFolders)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else {
|
||||
panic!("Expected RootFolders");
|
||||
};
|
||||
|
||||
assert_eq!(root_folders, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_status_event() {
|
||||
let status_json = json!({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use crate::models::lidarr_models::{DownloadsResponse, MetadataProfile, SystemStatus};
|
||||
use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag};
|
||||
use crate::models::lidarr_models::SystemStatus;
|
||||
use crate::models::servarr_models::DiskSpace;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
|
||||
@@ -24,64 +24,6 @@ impl Network<'_, '_> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_metadata_profiles(
|
||||
&mut self,
|
||||
) -> Result<Vec<MetadataProfile>> {
|
||||
info!("Fetching Lidarr metadata profiles");
|
||||
let event = LidarrEvent::GetMetadataProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<MetadataProfile>>(request_props, |metadata_profiles, mut app| {
|
||||
app.data.lidarr_data.metadata_profile_map = metadata_profiles
|
||||
.into_iter()
|
||||
.map(|profile| (profile.id, profile.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_quality_profiles(
|
||||
&mut self,
|
||||
) -> Result<Vec<QualityProfile>> {
|
||||
info!("Fetching Lidarr quality profiles");
|
||||
let event = LidarrEvent::GetQualityProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
|
||||
app.data.lidarr_data.quality_profile_map = quality_profiles
|
||||
.into_iter()
|
||||
.map(|profile| (profile.id, profile.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_tags(&mut self) -> Result<Vec<Tag>> {
|
||||
info!("Fetching Lidarr tags");
|
||||
let event = LidarrEvent::GetTags;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
|
||||
app.data.lidarr_data.tags_map = tags_vec
|
||||
.into_iter()
|
||||
.map(|tag| (tag.id, tag.label))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_diskspace(
|
||||
&mut self,
|
||||
) -> Result<Vec<DiskSpace>> {
|
||||
@@ -99,51 +41,6 @@ impl Network<'_, '_> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_downloads(
|
||||
&mut self,
|
||||
count: u64,
|
||||
) -> Result<DownloadsResponse> {
|
||||
info!("Fetching Lidarr downloads");
|
||||
let event = LidarrEvent::GetDownloads(count);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("pageSize={count}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| {
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.downloads
|
||||
.set_items(queue_response.records);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders(
|
||||
&mut self,
|
||||
) -> Result<Vec<RootFolder>> {
|
||||
info!("Fetching Lidarr root folders");
|
||||
let event = LidarrEvent::GetRootFolders;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
|
||||
app.data.lidarr_data.root_folders.set_items(root_folders);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_status(
|
||||
&mut self,
|
||||
) -> Result<SystemStatus> {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS};
|
||||
use crate::ui::DrawUi;
|
||||
use crate::ui::widgets::checkbox::Checkbox;
|
||||
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
|
||||
use crate::ui::widgets::popup::{Popup, Size};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_artist_ui_tests.rs"]
|
||||
mod delete_artist_ui_tests;
|
||||
|
||||
pub(in crate::ui::lidarr_ui) struct DeleteArtistUi;
|
||||
|
||||
impl DrawUi for DeleteArtistUi {
|
||||
fn accepts(route: Route) -> bool {
|
||||
let Route::Lidarr(active_lidarr_block, _) = route else {
|
||||
return false;
|
||||
};
|
||||
DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block)
|
||||
}
|
||||
|
||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
|
||||
if matches!(
|
||||
app.get_current_route(),
|
||||
Route::Lidarr(ActiveLidarrBlock::DeleteArtistPrompt, _)
|
||||
) {
|
||||
let selected_block = app.data.lidarr_data.selected_block.get_active_block();
|
||||
let prompt = format!(
|
||||
"Do you really want to delete the artist: \n{}?",
|
||||
app.data.lidarr_data.artists.current_selection().artist_name.text
|
||||
);
|
||||
let checkboxes = vec![
|
||||
Checkbox::new("Delete Artist Files")
|
||||
.checked(app.data.lidarr_data.delete_artist_files)
|
||||
.highlighted(selected_block == ActiveLidarrBlock::DeleteArtistToggleDeleteFile),
|
||||
Checkbox::new("Add List Exclusion")
|
||||
.checked(app.data.lidarr_data.add_import_list_exclusion)
|
||||
.highlighted(selected_block == ActiveLidarrBlock::DeleteArtistToggleAddListExclusion),
|
||||
];
|
||||
let confirmation_prompt = ConfirmationPrompt::new()
|
||||
.title("Delete Artist")
|
||||
.prompt(&prompt)
|
||||
.checkboxes(checkboxes)
|
||||
.yes_no_highlighted(selected_block == ActiveLidarrBlock::DeleteArtistConfirmPrompt)
|
||||
.yes_no_value(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
f.render_widget(
|
||||
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
|
||||
f.area(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::ui::DrawUi;
|
||||
use crate::ui::lidarr_ui::library::delete_artist_ui::DeleteArtistUi;
|
||||
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_ui_accepts() {
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(DeleteArtistUi::accepts(active_lidarr_block.into()));
|
||||
} else {
|
||||
assert!(!DeleteArtistUi::accepts(active_lidarr_block.into()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mod snapshot_tests {
|
||||
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_ui_renders_delete_artist() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
DeleteArtistUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,250 @@
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS};
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, LIBRARY_BLOCKS,
|
||||
};
|
||||
use crate::ui::lidarr_ui::library::{LibraryUi, decorate_artist_row_with_style};
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::DrawUi;
|
||||
use crate::ui::lidarr_ui::library::LibraryUi;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::widgets::{Cell, Row};
|
||||
|
||||
#[test]
|
||||
fn test_library_ui_accepts() {
|
||||
for lidarr_block in ActiveLidarrBlock::iter() {
|
||||
if LIBRARY_BLOCKS.contains(&lidarr_block) {
|
||||
assert!(LibraryUi::accepts(Route::Lidarr(lidarr_block, None)));
|
||||
let mut library_ui_blocks = Vec::new();
|
||||
library_ui_blocks.extend(LIBRARY_BLOCKS);
|
||||
library_ui_blocks.extend(DELETE_ARTIST_BLOCKS);
|
||||
for active_lidarr_block in ActiveLidarrBlock::iter() {
|
||||
if library_ui_blocks.contains(&active_lidarr_block) {
|
||||
assert!(LibraryUi::accepts(active_lidarr_block.into()));
|
||||
} else {
|
||||
assert!(!LibraryUi::accepts(Route::Lidarr(lidarr_block, None)));
|
||||
assert!(!LibraryUi::accepts(active_lidarr_block.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_unmonitored() {
|
||||
let artist = Artist::default();
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.unmonitored());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_downloaded_when_ended_and_all_tracks_present() {
|
||||
let artist = Artist {
|
||||
monitored: true,
|
||||
status: ArtistStatus::Ended,
|
||||
statistics: Some(ArtistStatistics {
|
||||
track_file_count: 10,
|
||||
total_track_count: 10,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
};
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.downloaded());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_missing_when_ended_and_tracks_are_missing() {
|
||||
let artist = Artist {
|
||||
monitored: true,
|
||||
status: ArtistStatus::Ended,
|
||||
statistics: Some(ArtistStatistics {
|
||||
track_file_count: 5,
|
||||
total_track_count: 10,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
};
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.missing());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_indeterminate_when_ended_and_no_statistics() {
|
||||
let artist = Artist {
|
||||
monitored: true,
|
||||
status: ArtistStatus::Ended,
|
||||
statistics: None,
|
||||
..Artist::default()
|
||||
};
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.indeterminate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_indeterminate_when_ended_and_total_track_count_is_zero() {
|
||||
let artist = Artist {
|
||||
monitored: true,
|
||||
status: ArtistStatus::Ended,
|
||||
statistics: Some(ArtistStatistics {
|
||||
track_file_count: 0,
|
||||
total_track_count: 0,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
};
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.missing());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_unreleased_when_continuing_and_all_tracks_present() {
|
||||
let artist = Artist {
|
||||
monitored: true,
|
||||
status: ArtistStatus::Continuing,
|
||||
statistics: Some(ArtistStatistics {
|
||||
track_file_count: 10,
|
||||
total_track_count: 10,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
};
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.unreleased());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_missing_when_continuing_and_tracks_are_missing() {
|
||||
let artist = Artist {
|
||||
monitored: true,
|
||||
status: ArtistStatus::Continuing,
|
||||
statistics: Some(ArtistStatistics {
|
||||
track_file_count: 5,
|
||||
total_track_count: 10,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
};
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.missing());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_indeterminate_when_continuing_and_no_statistics() {
|
||||
let artist = Artist {
|
||||
monitored: true,
|
||||
status: ArtistStatus::Continuing,
|
||||
statistics: None,
|
||||
..Artist::default()
|
||||
};
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.indeterminate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorate_row_with_style_defaults_to_indeterminate_for_deleted_status() {
|
||||
let artist = Artist {
|
||||
monitored: true,
|
||||
status: ArtistStatus::Deleted,
|
||||
..Artist::default()
|
||||
};
|
||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||
|
||||
let style = decorate_artist_row_with_style(&artist, row.clone());
|
||||
|
||||
assert_eq!(style, row.indeterminate());
|
||||
}
|
||||
|
||||
mod snapshot_tests {
|
||||
use crate::app::App;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::ui::lidarr_ui::library::LibraryUi;
|
||||
use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app};
|
||||
use crate::ui::DrawUi;
|
||||
|
||||
#[rstest]
|
||||
fn test_library_ui_renders(
|
||||
#[values(
|
||||
ActiveLidarrBlock::Artists,
|
||||
ActiveLidarrBlock::ArtistsSortPrompt,
|
||||
ActiveLidarrBlock::SearchArtists,
|
||||
ActiveLidarrBlock::SearchArtistsError,
|
||||
ActiveLidarrBlock::FilterArtists,
|
||||
ActiveLidarrBlock::FilterArtistsError
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
LibraryUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(format!("lidarr_library_{active_lidarr_block}"), output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_ui_renders_loading() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
LibraryUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_ui_renders_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
LibraryUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_ui_renders_delete_artist_over_library() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
LibraryUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use delete_artist_ui::DeleteArtistUi;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Rect},
|
||||
@@ -20,6 +21,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
mod delete_artist_ui;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "library_ui_tests.rs"]
|
||||
mod library_ui_tests;
|
||||
@@ -29,14 +32,19 @@ pub(super) struct LibraryUi;
|
||||
impl DrawUi for LibraryUi {
|
||||
fn accepts(route: Route) -> bool {
|
||||
if let Route::Lidarr(active_lidarr_block, _) = route {
|
||||
return LIBRARY_BLOCKS.contains(&active_lidarr_block);
|
||||
return DeleteArtistUi::accepts(route) || LIBRARY_BLOCKS.contains(&active_lidarr_block);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
let route = app.get_current_route();
|
||||
draw_library(f, app, area);
|
||||
|
||||
if DeleteArtistUi::accepts(route) {
|
||||
DeleteArtistUi::draw(f, app, area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/delete_artist_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭───────────────────── Delete Artist ─────────────────────╮
|
||||
│ Do you really want to delete the artist: │
|
||||
│ ? │
|
||||
│ │
|
||||
│ │
|
||||
│ ╭───╮ │
|
||||
│ Delete Artist Files: │ │ │
|
||||
│ ╰───╯ │
|
||||
│ ╭───╮ │
|
||||
│ Add List Exclusion: │ │ │
|
||||
│ ╰───╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│╭────────────────────────────╮╭───────────────────────────╮│
|
||||
││ Yes ││ No ││
|
||||
│╰────────────────────────────╯╰───────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────╯
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭───────────────────── Delete Artist ─────────────────────╮
|
||||
│ Do you really want to delete the artist: │
|
||||
│ ? │
|
||||
│ │
|
||||
│ │
|
||||
│ ╭───╮ │
|
||||
│ Delete Artist Files: │ │ │
|
||||
│ ╰───╯ │
|
||||
│ ╭───╮ │
|
||||
│ Add List Exclusion: │ │ │
|
||||
│ ╰───╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│╭────────────────────────────╮╭───────────────────────────╮│
|
||||
││ Yes ││ No ││
|
||||
│╰────────────────────────────╯╰───────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────╯
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Loading ...
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭───────────────────────────────╮
|
||||
│Name │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
╰───────────────────────────────╯
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭───────────────── Filter ──────────────────╮
|
||||
│artist filter │
|
||||
╰─────────────────────────────────────────────╯
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭─────────────── Error ───────────────╮
|
||||
│The given filter produced empty results│
|
||||
│ │
|
||||
╰───────────────────────────────────────╯
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭───────────────── Search ──────────────────╮
|
||||
│artist search │
|
||||
╰─────────────────────────────────────────────╯
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭─────────────── Error ───────────────╮
|
||||
│ No items found matching search │
|
||||
│ │
|
||||
╰───────────────────────────────────────╯
|
||||
Reference in New Issue
Block a user