feat: Completed support for viewing Lidarr artist details
This commit is contained in:
@@ -5,7 +5,7 @@ use crate::app::context_clues::{
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS,
|
||||
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -36,6 +36,25 @@ pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 7] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub(in crate::app) struct LidarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for LidarrContextClueProvider {
|
||||
@@ -45,6 +64,11 @@ impl ContextClueProvider for LidarrContextClueProvider {
|
||||
};
|
||||
|
||||
match active_lidarr_block {
|
||||
_ if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_info_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults => {
|
||||
Some(&BARE_POPUP_CONTEXT_CLUES)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider,
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
|
||||
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
|
||||
LidarrContextClueProvider,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, LidarrData,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use rstest::rstest;
|
||||
|
||||
@@ -65,6 +68,50 @@ mod tests {
|
||||
assert_none!(artists_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_context_clues() {
|
||||
let mut artist_details_context_clues_iter = ARTIST_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(artist_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_search_results_context_clues() {
|
||||
let mut add_artist_search_results_context_clues_iter =
|
||||
@@ -92,6 +139,23 @@ mod tests {
|
||||
LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
|
||||
fn test_lidarr_context_clue_provider_artist_info_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data = LidarrData::default();
|
||||
app.data.lidarr_data.artist_info_tabs.set_index(index);
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
@@ -4,6 +4,7 @@ mod tests {
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::network::NetworkEvent;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -32,6 +33,23 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_artist_details() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into());
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
@@ -60,4 +78,12 @@ mod tests {
|
||||
|
||||
assert_str_eq!(query, "Test Artist");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_artist_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
|
||||
assert_eq!(app.extract_artist_id().await, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ impl App<'_> {
|
||||
.dispatch_network_event(LidarrEvent::ListArtists.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::ArtistDetails => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistSearchResults => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
@@ -53,6 +58,10 @@ impl App<'_> {
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn extract_artist_id(&self) -> i64 {
|
||||
self.data.lidarr_data.artists.current_selection().id
|
||||
}
|
||||
|
||||
async fn check_for_lidarr_prompt_action(&mut self) {
|
||||
if self.data.lidarr_data.prompt_confirm {
|
||||
self.data.lidarr_data.prompt_confirm = false;
|
||||
|
||||
@@ -18,6 +18,15 @@ mod get_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrGetCommand {
|
||||
#[command(about = "Get detailed information for the album with the given ID")]
|
||||
AlbumDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the album whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(about = "Get detailed information for the artist with the given ID")]
|
||||
ArtistDetails {
|
||||
#[arg(
|
||||
@@ -62,6 +71,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHan
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrGetCommand::AlbumDetails { album_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAlbumDetails(album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::ArtistDetails { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
|
||||
@@ -23,6 +23,32 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_album_details_requires_album_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "album-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"get",
|
||||
"album-details",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_requires_artist_id() {
|
||||
let result =
|
||||
@@ -91,6 +117,32 @@ mod tests {
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_album_details_command() {
|
||||
let expected_album_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetAlbumDetails(expected_album_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_album_details_command = LidarrGetCommand::AlbumDetails { album_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_album_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_artist_details_command() {
|
||||
let expected_artist_id = 1;
|
||||
|
||||
@@ -75,6 +75,31 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_album_monitoring_requires_album_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-album-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_album_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"toggle-album-monitoring",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_artist_requires_query() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "search-new-artist"]);
|
||||
@@ -110,6 +135,7 @@ mod tests {
|
||||
use crate::cli::lidarr::add_command_handler::LidarrAddCommand;
|
||||
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
|
||||
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
|
||||
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
@@ -306,5 +332,37 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
|
||||
{
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist {
|
||||
artist_id: 1,
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use clap::{Subcommand, arg};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
@@ -18,6 +18,15 @@ mod list_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrListCommand {
|
||||
#[command(about = "List all albums for the artist with the given ID")]
|
||||
Albums {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist whose albums you want to list",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
#[command(about = "List all artists in your Lidarr library")]
|
||||
Artists,
|
||||
#[command(about = "List all Lidarr metadata profiles")]
|
||||
@@ -55,6 +64,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrListCommand::Albums { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAlbums(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Artists => {
|
||||
let resp = self
|
||||
.network
|
||||
|
||||
@@ -19,6 +19,8 @@ mod tests {
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
@@ -29,6 +31,32 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_albums_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "albums"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_albums_with_artist_id() {
|
||||
let expected_args = LidarrListCommand::Albums { artist_id: 1 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "lidarr", "list", "albums", "--artist-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(album_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(album_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
@@ -77,5 +105,27 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_albums_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetAlbums(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_command = LidarrListCommand::Albums { artist_id: 1 };
|
||||
|
||||
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
|
||||
use list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
|
||||
use tokio::sync::Mutex;
|
||||
use trigger_automatic_search_command_handler::{
|
||||
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
@@ -21,6 +24,7 @@ mod edit_command_handler;
|
||||
mod get_command_handler;
|
||||
mod list_command_handler;
|
||||
mod refresh_command_handler;
|
||||
mod trigger_automatic_search_command_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_command_tests.rs"]
|
||||
@@ -58,6 +62,11 @@ pub enum LidarrCommand {
|
||||
about = "Commands to refresh the data in your Lidarr instance"
|
||||
)]
|
||||
Refresh(LidarrRefreshCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance"
|
||||
)]
|
||||
TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand),
|
||||
#[command(about = "Search for a new artist to add to Lidarr")]
|
||||
SearchNewArtist {
|
||||
#[arg(
|
||||
@@ -67,6 +76,17 @@ pub enum LidarrCommand {
|
||||
)]
|
||||
query: String,
|
||||
},
|
||||
#[command(
|
||||
about = "Toggle monitoring for the specified album corresponding to the given album ID"
|
||||
)]
|
||||
ToggleAlbumMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the album to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Toggle monitoring for the specified artist corresponding to the given artist ID"
|
||||
)]
|
||||
@@ -137,6 +157,15 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
|
||||
LidarrTriggerAutomaticSearchCommandHandler::with(
|
||||
self.app,
|
||||
trigger_automatic_search_command,
|
||||
self.network,
|
||||
)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::SearchNewArtist { query } => {
|
||||
let resp = self
|
||||
.network
|
||||
@@ -144,6 +173,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::ToggleAlbumMonitoring { album_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ToggleAlbumMonitoring(album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::ToggleArtistMonitoring { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
|
||||
@@ -19,6 +19,15 @@ mod refresh_command_handler_tests;
|
||||
pub enum LidarrRefreshCommand {
|
||||
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
|
||||
AllArtists,
|
||||
#[command(about = "Refresh artist data and scan disk for the artist with the given ID")]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the artist to refresh information on and to scan the disk for",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrRefreshCommand> for Command {
|
||||
@@ -57,6 +66,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrRefreshCommand::Artist { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::UpdateAndScanArtist(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -20,6 +20,8 @@ mod tests {
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_refresh_all_artists_has_no_arg_requirements() {
|
||||
@@ -28,6 +30,37 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_artist_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_artist_with_artist_id() {
|
||||
let expected_args = LidarrRefreshCommand::Artist { artist_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"refresh",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Lidarr(LidarrCommand::Refresh(refresh_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
@@ -68,5 +101,29 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_artist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::UpdateAndScanArtist(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = LidarrRefreshCommand::Artist { artist_id: 1 };
|
||||
|
||||
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "trigger_automatic_search_command_handler_tests.rs"]
|
||||
mod trigger_automatic_search_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrTriggerAutomaticSearchCommand {
|
||||
#[command(about = "Trigger an automatic search for the artist with the specified ID")]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the artist you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrTriggerAutomaticSearchCommand> for Command {
|
||||
fn from(value: LidarrTriggerAutomaticSearchCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrTriggerAutomaticSearchCommand>
|
||||
for LidarrTriggerAutomaticSearchCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrTriggerAutomaticSearchCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrTriggerAutomaticSearchCommand::Artist { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::TriggerAutomaticArtistSearch(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{
|
||||
LidarrCommand, trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand,
|
||||
},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_trigger_automatic_search_command_from() {
|
||||
let command = LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(command))
|
||||
);
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_artist_search_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"trigger-automatic-search",
|
||||
"artist",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_artist_search_with_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"trigger-automatic-search",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::lidarr::trigger_automatic_search_command_handler::{
|
||||
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
use crate::{app::App, cli::CliCommandHandler};
|
||||
use crate::{
|
||||
models::{Serdeable, lidarr_models::LidarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_artist_search_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
|
||||
|
||||
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::lidarr_models::Album;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "artist_details_handler_tests.rs"]
|
||||
mod artist_details_handler_tests;
|
||||
|
||||
pub struct ArtistDetailsHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl ArtistDetailsHandler<'_, '_> {
|
||||
fn extract_artist_id(&self) -> i64 {
|
||||
self.app.data.lidarr_data.artists.current_selection().id
|
||||
}
|
||||
|
||||
fn extract_album_id(&self) -> i64 {
|
||||
self.app.data.lidarr_data.albums.current_selection().id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let albums_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveLidarrBlock::ArtistDetails.into())
|
||||
.searching_block(ActiveLidarrBlock::SearchAlbums.into())
|
||||
.search_error_block(ActiveLidarrBlock::SearchAlbumsError.into())
|
||||
.search_field_fn(|album: &Album| &album.title.text);
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.lidarr_data.albums,
|
||||
albums_table_handling_config,
|
||||
) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
ARTIST_DETAILS_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
) -> ArtistDetailsHandler<'a, 'b> {
|
||||
ArtistDetailsHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
handle_prompt_toggle(self.app, self.key);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()),
|
||||
);
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id()));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
ActiveLidarrBlock::ArtistDetails => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.reset_artist_info_tabs();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::ArtistDetails => match self.key {
|
||||
_ if matches_key!(refresh, key) => self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_lidarr_block.into()),
|
||||
_ if matches_key!(auto_search, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into());
|
||||
}
|
||||
_ if matches_key!(update, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
|
||||
}
|
||||
_ if matches_key!(edit, key) => {
|
||||
self.app.push_navigation_stack(
|
||||
(
|
||||
ActiveLidarrBlock::EditArtistPrompt,
|
||||
Some(self.active_lidarr_block),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into());
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
|
||||
}
|
||||
_ if matches_key!(toggle_monitoring, key) => {
|
||||
if !self.app.data.lidarr_data.albums.is_empty() {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::ToggleAlbumMonitoring(self.extract_album_id()));
|
||||
|
||||
self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()),
|
||||
);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id()));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, ARTIST_DETAILS_BLOCKS,
|
||||
};
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
|
||||
#[rstest]
|
||||
fn test_left_right_prompt_toggle(
|
||||
#[values(
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
)] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[values(Key::Left, Key::Right)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use rstest::rstest;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[rstest]
|
||||
#[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, LidarrEvent::TriggerAutomaticArtistSearch(1))]
|
||||
#[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, LidarrEvent::UpdateAndScanArtist(1))]
|
||||
fn test_artist_details_prompt_confirm_submit(
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
#[case] expected_action: LidarrEvent
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
prompt_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_prompt_decline_submit(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
|
||||
)] prompt_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
prompt_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use rstest::rstest;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_esc(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
)] prompt_block: ActiveLidarrBlock,
|
||||
#[values(true, false)] is_ready: bool
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_char_key_event {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped};
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::lidarr_models::{Artist};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_edit_key(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.edit.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(
|
||||
app,
|
||||
(
|
||||
ActiveLidarrBlock::EditArtistPrompt,
|
||||
Some(ActiveLidarrBlock::ArtistDetails)
|
||||
)
|
||||
.into()
|
||||
);
|
||||
assert_modal_present!(app.data.lidarr_data.edit_artist_modal);
|
||||
assert!(app.data.lidarr_data.edit_artist_modal.is_some());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.blocks,
|
||||
EDIT_ARTIST_SELECTION_BLOCKS
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_edit_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.edit.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
assert_modal_absent!(app.data.lidarr_data.edit_artist_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_toggle_monitoring_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_routing = false;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(app.is_routing);
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::ToggleAlbumMonitoring(1))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_toggle_monitoring_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.lidarr_data.prompt_confirm = false;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_toggle_monitoring_key_no_op_when_albums_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(app.data.lidarr_data.prompt_confirm_action.is_none());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_auto_search_key(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.auto_search.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(
|
||||
app,
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_auto_search_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.auto_search.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_update_key(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.update.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_update_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.update.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_refresh_key(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_routing = false;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(
|
||||
app,
|
||||
active_lidarr_block.into()
|
||||
);
|
||||
assert!(app.is_routing);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_refresh_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.is_routing = false;
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
active_lidarr_block.into()
|
||||
);
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(1)
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
|
||||
LidarrEvent::UpdateAndScanArtist(1)
|
||||
)]
|
||||
fn test_artist_details_prompt_confirm_key(
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
#[case] expected_action: LidarrEvent,
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
prompt_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert_navigation_popped!(app, active_lidarr_block.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_handler_accepts() {
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(ArtistDetailsHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!ArtistDetailsHandler::accepts(active_lidarr_block));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_artist_id() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
|
||||
let artist_id = ArtistDetailsHandler::new(DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app, ActiveLidarrBlock::ArtistDetails, None).extract_artist_id();
|
||||
|
||||
assert_eq!(artist_id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_album_id() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
|
||||
let album_id = ArtistDetailsHandler::new(DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app, ActiveLidarrBlock::ArtistDetails, None).extract_album_id();
|
||||
|
||||
assert_eq!(album_id, 1);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
|
||||
let handler = ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_handler_is_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_handler_is_ready_when_not_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ mod tests {
|
||||
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::{
|
||||
ADD_ARTIST_BLOCKS, ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS,
|
||||
EDIT_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
|
||||
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ARTIST_BLOCKS,
|
||||
EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
@@ -26,6 +26,7 @@ mod tests {
|
||||
fn test_library_handler_accepts() {
|
||||
let mut library_handler_blocks = Vec::new();
|
||||
library_handler_blocks.extend(LIBRARY_BLOCKS);
|
||||
library_handler_blocks.extend(ARTIST_DETAILS_BLOCKS);
|
||||
library_handler_blocks.extend(DELETE_ARTIST_BLOCKS);
|
||||
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
|
||||
library_handler_blocks.extend(ADD_ARTIST_BLOCKS);
|
||||
@@ -587,6 +588,32 @@ mod tests {
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_artist_details_blocks_to_artist_details_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
ActiveLidarrBlock::SearchAlbums,
|
||||
ActiveLidarrBlock::SearchAlbumsError,
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_key() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
@@ -19,11 +19,13 @@ use super::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
|
||||
mod add_artist_handler;
|
||||
mod artist_details_handler;
|
||||
mod delete_artist_handler;
|
||||
mod edit_artist_handler;
|
||||
|
||||
use crate::models::Route;
|
||||
pub(in crate::handlers::lidarr_handlers) use add_artist_handler::AddArtistHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use artist_details_handler::ArtistDetailsHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler;
|
||||
|
||||
@@ -74,6 +76,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
|
||||
EditArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if ArtistDetailsHandler::accepts(self.active_lidarr_block) => {
|
||||
ArtistDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
}
|
||||
}
|
||||
@@ -83,6 +89,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
|
||||
AddArtistHandler::accepts(active_block)
|
||||
|| DeleteArtistHandler::accepts(active_block)
|
||||
|| EditArtistHandler::accepts(active_block)
|
||||
|| ArtistDetailsHandler::accepts(active_block)
|
||||
|| LIBRARY_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
@@ -139,12 +146,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::UpdateAllArtistsPrompt {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::Artists => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
}
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ mod tests {
|
||||
ActiveSonarrBlock::SeriesDetails.into()
|
||||
);
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_modal_absent!(app.data.sonarr_data.prompt_confirm_action);
|
||||
assert_none!(app.data.sonarr_data.prompt_confirm_action);
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,14 @@ pub struct AddArtistSearchResult {
|
||||
pub ratings: Option<Ratings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LidarrCommandBody {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub artist_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct DeleteArtistParams {
|
||||
@@ -291,6 +299,44 @@ pub struct EditArtistParams {
|
||||
pub clear_tags: bool,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Album {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub title: HorizontallyScrollableText,
|
||||
pub foreign_album_id: String,
|
||||
pub monitored: bool,
|
||||
#[serde(default)]
|
||||
pub any_release_ok: bool,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub profile_id: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub duration: i64,
|
||||
pub album_type: Option<String>,
|
||||
pub genres: Vec<String>,
|
||||
pub ratings: Option<Ratings>,
|
||||
pub release_date: Option<DateTime<Utc>>,
|
||||
pub statistics: Option<AlbumStatistics>,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AlbumStatistics {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub track_file_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub track_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub total_track_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub size_on_disk: i64,
|
||||
#[serde(deserialize_with = "super::from_f64")]
|
||||
pub percent_of_tracks: f64,
|
||||
}
|
||||
|
||||
impl Eq for AlbumStatistics {}
|
||||
|
||||
impl From<LidarrSerdeable> for Serdeable {
|
||||
fn from(value: LidarrSerdeable) -> Serdeable {
|
||||
Serdeable::Lidarr(value)
|
||||
@@ -300,6 +346,8 @@ impl From<LidarrSerdeable> for Serdeable {
|
||||
serde_enum_from!(
|
||||
LidarrSerdeable {
|
||||
AddArtistSearchResults(Vec<AddArtistSearchResult>),
|
||||
Albums(Vec<Album>),
|
||||
Album(Album),
|
||||
Artist(Artist),
|
||||
Artists(Vec<Artist>),
|
||||
DiskSpaces(Vec<DiskSpace>),
|
||||
|
||||
@@ -5,7 +5,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::models::lidarr_models::{
|
||||
AddArtistSearchResult, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
|
||||
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
|
||||
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
|
||||
};
|
||||
use crate::models::servarr_models::{
|
||||
@@ -401,6 +401,45 @@ mod tests {
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Tags(tags));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_add_artist_search_results() {
|
||||
let search_results = vec![AddArtistSearchResult {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
..AddArtistSearchResult::default()
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = search_results.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::AddArtistSearchResults(search_results)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_albums() {
|
||||
let albums = vec![Album {
|
||||
id: 1,
|
||||
..Album::default()
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = albums.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Albums(albums));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_album() {
|
||||
let album = Album {
|
||||
id: 1,
|
||||
..Album::default()
|
||||
};
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = album.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Album(album));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_status_display() {
|
||||
assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing");
|
||||
@@ -501,19 +540,4 @@ mod tests {
|
||||
assert!(search_result.genres.is_empty());
|
||||
assert_none!(&search_result.ratings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_add_artist_search_results() {
|
||||
let search_results = vec![AddArtistSearchResult {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
..AddArtistSearchResult::default()
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = search_results.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::AddArtistSearchResults(search_results)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use serde_json::Number;
|
||||
|
||||
use super::modals::{AddArtistModal, EditArtistModal};
|
||||
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::{
|
||||
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
|
||||
lidarr_models::{AddArtistSearchResult, Artist, DownloadRecord},
|
||||
lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord},
|
||||
servarr_models::{DiskSpace, RootFolder},
|
||||
stateful_table::StatefulTable,
|
||||
};
|
||||
@@ -19,8 +21,8 @@ use {
|
||||
crate::models::stateful_table::SortOption,
|
||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map,
|
||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
add_artist_search_result, download_record, metadata_profile, metadata_profile_map,
|
||||
quality_profile, root_folder, tags_map,
|
||||
add_artist_search_result, album, artist, download_record, metadata_profile,
|
||||
metadata_profile_map, quality_profile, root_folder, tags_map,
|
||||
},
|
||||
crate::network::servarr_test_utils::diskspace,
|
||||
strum::{Display, EnumString, IntoEnumIterator},
|
||||
@@ -35,6 +37,8 @@ pub struct LidarrData<'a> {
|
||||
pub add_artist_search: Option<HorizontallyScrollableText>,
|
||||
pub add_import_list_exclusion: bool,
|
||||
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
|
||||
pub albums: StatefulTable<Album>,
|
||||
pub artist_info_tabs: TabState,
|
||||
pub artists: StatefulTable<Artist>,
|
||||
pub delete_artist_files: bool,
|
||||
pub disk_space_vec: Vec<DiskSpace>,
|
||||
@@ -58,6 +62,11 @@ impl LidarrData<'_> {
|
||||
self.add_import_list_exclusion = false;
|
||||
}
|
||||
|
||||
pub fn reset_artist_info_tabs(&mut self) {
|
||||
self.albums = StatefulTable::default();
|
||||
self.artist_info_tabs.index = 0;
|
||||
}
|
||||
|
||||
pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String {
|
||||
tag_ids
|
||||
.iter()
|
||||
@@ -97,6 +106,7 @@ impl<'a> Default for LidarrData<'a> {
|
||||
add_artist_search: None,
|
||||
add_import_list_exclusion: false,
|
||||
add_searched_artists: None,
|
||||
albums: StatefulTable::default(),
|
||||
artists: StatefulTable::default(),
|
||||
delete_artist_files: false,
|
||||
disk_space_vec: Vec::new(),
|
||||
@@ -117,6 +127,12 @@ impl<'a> Default for LidarrData<'a> {
|
||||
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
|
||||
config: None,
|
||||
}]),
|
||||
artist_info_tabs: TabState::new(vec![TabRoute {
|
||||
title: "Albums".to_string(),
|
||||
route: ActiveLidarrBlock::ArtistDetails.into(),
|
||||
contextual_help: Some(&ARTIST_DETAILS_CONTEXT_CLUES),
|
||||
config: None,
|
||||
}]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +185,9 @@ impl LidarrData<'_> {
|
||||
tags_map: tags_map(),
|
||||
..LidarrData::default()
|
||||
};
|
||||
lidarr_data.artists.set_items(vec![Artist::default()]);
|
||||
lidarr_data.albums.set_items(vec![album()]);
|
||||
lidarr_data.albums.search = Some("album search".into());
|
||||
lidarr_data.artists.set_items(vec![artist()]);
|
||||
lidarr_data.artists.sorting(vec![SortOption {
|
||||
name: "Name",
|
||||
cmp_fn: Some(|a: &Artist, b: &Artist| a.artist_name.text.cmp(&b.artist_name.text)),
|
||||
@@ -193,6 +211,7 @@ impl LidarrData<'_> {
|
||||
pub enum ActiveLidarrBlock {
|
||||
#[default]
|
||||
Artists,
|
||||
ArtistDetails,
|
||||
ArtistsSortPrompt,
|
||||
AddArtistAlreadyInLibrary,
|
||||
AddArtistConfirmPrompt,
|
||||
@@ -206,6 +225,7 @@ pub enum ActiveLidarrBlock {
|
||||
AddArtistSelectQualityProfile,
|
||||
AddArtistSelectRootFolder,
|
||||
AddArtistTagsInput,
|
||||
AutomaticallySearchArtistPrompt,
|
||||
DeleteArtistPrompt,
|
||||
DeleteArtistConfirmPrompt,
|
||||
DeleteArtistToggleDeleteFile,
|
||||
@@ -220,9 +240,12 @@ pub enum ActiveLidarrBlock {
|
||||
EditArtistToggleMonitored,
|
||||
FilterArtists,
|
||||
FilterArtistsError,
|
||||
SearchAlbums,
|
||||
SearchAlbumsError,
|
||||
SearchArtists,
|
||||
SearchArtistsError,
|
||||
UpdateAllArtistsPrompt,
|
||||
UpdateAndScanArtistPrompt,
|
||||
}
|
||||
|
||||
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
|
||||
@@ -235,6 +258,14 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
];
|
||||
|
||||
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
ActiveLidarrBlock::SearchAlbums,
|
||||
ActiveLidarrBlock::SearchAlbumsError,
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
|
||||
];
|
||||
|
||||
pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 12] = [
|
||||
ActiveLidarrBlock::AddArtistAlreadyInLibrary,
|
||||
ActiveLidarrBlock::AddArtistConfirmPrompt,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::lidarr_models::Album;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS,
|
||||
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ARTIST_BLOCKS,
|
||||
DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::{
|
||||
@@ -44,6 +47,18 @@ mod tests {
|
||||
assert!(!lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_artist_info_tabs() {
|
||||
let mut lidarr_data = LidarrData::default();
|
||||
lidarr_data.albums.set_items(vec![Album::default()]);
|
||||
lidarr_data.artist_info_tabs.index = 1;
|
||||
|
||||
lidarr_data.reset_artist_info_tabs();
|
||||
|
||||
assert_is_empty!(lidarr_data.albums);
|
||||
assert_eq!(lidarr_data.artist_info_tabs.index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag_ids_to_display() {
|
||||
let mut tags_map = BiMap::new();
|
||||
@@ -112,6 +127,7 @@ mod tests {
|
||||
assert_none!(lidarr_data.add_artist_search);
|
||||
assert!(!lidarr_data.add_import_list_exclusion);
|
||||
assert_none!(lidarr_data.add_searched_artists);
|
||||
assert_is_empty!(lidarr_data.albums);
|
||||
assert_is_empty!(lidarr_data.artists);
|
||||
assert!(!lidarr_data.delete_artist_files);
|
||||
assert_is_empty!(lidarr_data.disk_space_vec);
|
||||
@@ -139,6 +155,18 @@ mod tests {
|
||||
&ARTISTS_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.main_tabs.tabs[0].config);
|
||||
|
||||
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1);
|
||||
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
|
||||
assert_eq!(
|
||||
lidarr_data.artist_info_tabs.tabs[0].route,
|
||||
ActiveLidarrBlock::ArtistDetails.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&lidarr_data.artist_info_tabs.tabs[0].contextual_help,
|
||||
&ARTIST_DETAILS_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.artist_info_tabs.tabs[0].config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -153,6 +181,16 @@ mod tests {
|
||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::UpdateAllArtistsPrompt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_blocks_contains_expected_blocks() {
|
||||
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 5);
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistDetails));
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchArtistPrompt));
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbums));
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumsError));
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_blocks_contents() {
|
||||
assert_eq!(ADD_ARTIST_BLOCKS.len(), 12);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mockito::Matcher;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::{json, Value};
|
||||
use crate::models::lidarr_models::{Album, LidarrSerdeable};
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ALBUM_JSON};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::network_tests::test_utils::{test_network, MockServarrApi};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_albums_event() {
|
||||
let albums_json = json!([{
|
||||
"id": 1,
|
||||
"title": "Test Album",
|
||||
"foreignAlbumId": "test-foreign-album-id",
|
||||
"monitored": true,
|
||||
"anyReleaseOk": true,
|
||||
"profileId": 1,
|
||||
"duration": 180,
|
||||
"albumType": "Album",
|
||||
"genres": ["Classical"],
|
||||
"ratings": {"votes": 15, "value": 8.4},
|
||||
"releaseDate": "2023-01-01T00:00:00Z",
|
||||
"statistics": {
|
||||
"trackFileCount": 10,
|
||||
"trackCount": 10,
|
||||
"totalTrackCount": 10,
|
||||
"sizeOnDisk": 1024,
|
||||
"percentOfTracks": 99.9
|
||||
}
|
||||
}]);
|
||||
let response: Vec<Album> = serde_json::from_value(albums_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(albums_json)
|
||||
.query("artistId=1")
|
||||
.build_for(LidarrEvent::GetAlbums(1))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::GetAlbums(1)).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::Albums(albums) = result.unwrap() else {
|
||||
panic!("Expected Albums");
|
||||
};
|
||||
|
||||
assert_eq!(albums, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.albums.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_toggle_album_monitoring_event() {
|
||||
let mut expected_body: Value = serde_json::from_str(ALBUM_JSON).unwrap();
|
||||
*expected_body.get_mut("monitored").unwrap() = json!(false);
|
||||
let (get_mock, app, mut server) = MockServarrApi::get()
|
||||
.returns(serde_json::from_str(ALBUM_JSON).unwrap())
|
||||
.path("/1")
|
||||
.build_for(LidarrEvent::GetAlbums(1))
|
||||
.await;
|
||||
let put_mock = server
|
||||
.mock("PUT", "/api/v1/album/1")
|
||||
.match_body(Matcher::Json(expected_body))
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.with_status(202)
|
||||
.create_async()
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
assert_ok!(
|
||||
network
|
||||
.handle_lidarr_event(LidarrEvent::ToggleAlbumMonitoring(1))
|
||||
.await
|
||||
);
|
||||
|
||||
get_mock.assert_async().await;
|
||||
put_mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_album_details_event() {
|
||||
let expected_album: Album = serde_json::from_str(ALBUM_JSON).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(serde_json::from_str(ALBUM_JSON).unwrap())
|
||||
.path("/1")
|
||||
.build_for(LidarrEvent::GetAlbumDetails(1))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetAlbumDetails(1))
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::Album(album) = result.unwrap() else {
|
||||
panic!("Expected Album");
|
||||
};
|
||||
|
||||
assert_eq!(album, expected_album);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::models::lidarr_models::{Album};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info, warn};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_albums_network_tests.rs"]
|
||||
mod lidarr_albums_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn get_albums(
|
||||
&mut self,
|
||||
artist_id: i64,
|
||||
) -> Result<Vec<Album>> {
|
||||
info!("Fetching albums for Lidarr artist with ID: {artist_id}");
|
||||
let event = LidarrEvent::GetAlbums(artist_id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("artistId={artist_id}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Album>>(request_props, |mut albums_vec, mut app| {
|
||||
albums_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.lidarr_data.albums.set_items(albums_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_album_details(
|
||||
&mut self,
|
||||
album_id: i64,
|
||||
) -> Result<Album> {
|
||||
info!("Fetching details for Lidarr album with ID: {album_id}");
|
||||
let event = LidarrEvent::GetAlbumDetails(album_id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{album_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Album>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn toggle_album_monitoring(
|
||||
&mut self,
|
||||
album_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = LidarrEvent::ToggleAlbumMonitoring(album_id);
|
||||
info!("Toggling album monitoring for album with ID: {album_id}");
|
||||
info!("Fetching album details for album with ID: {album_id}");
|
||||
|
||||
let detail_event = LidarrEvent::GetAlbums(0);
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{album_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut response = String::new();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_album_body, _| {
|
||||
response = detailed_album_body.to_string()
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Constructing toggle album monitoring body");
|
||||
|
||||
match serde_json::from_str::<Value>(&response) {
|
||||
Ok(mut detailed_album_body) => {
|
||||
let monitored = detailed_album_body
|
||||
.get("monitored")
|
||||
.unwrap()
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
|
||||
*detailed_album_body.get_mut("monitored").unwrap() = json!(!monitored);
|
||||
|
||||
debug!("Toggle album monitoring body: {detailed_album_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Put,
|
||||
Some(detailed_album_body),
|
||||
Some(format!("/{album_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Request for detailed album body was interrupted");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
-2
@@ -134,11 +134,10 @@ mod tests {
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
assert!(
|
||||
assert_ok!(
|
||||
network
|
||||
.handle_lidarr_event(LidarrEvent::ToggleArtistMonitoring(1))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
get_mock.assert_async().await;
|
||||
@@ -167,6 +166,52 @@ mod tests {
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_update_and_scan_artist_event() {
|
||||
let (mock, app, _server) = MockServarrApi::post()
|
||||
.with_request_body(json!({
|
||||
"name": "RefreshArtist",
|
||||
"artistId": 1
|
||||
}))
|
||||
.returns(json!({}))
|
||||
.build_for(LidarrEvent::UpdateAndScanArtist(1))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
assert!(
|
||||
network
|
||||
.handle_lidarr_event(LidarrEvent::UpdateAndScanArtist(1))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_artist_search_event() {
|
||||
let (mock, app, _server) = MockServarrApi::post()
|
||||
.with_request_body(json!({
|
||||
"name": "ArtistSearch",
|
||||
"artistId": 1
|
||||
}))
|
||||
.returns(json!({}))
|
||||
.build_for(LidarrEvent::TriggerAutomaticArtistSearch(1))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
assert!(
|
||||
network
|
||||
.handle_lidarr_event(LidarrEvent::TriggerAutomaticArtistSearch(1))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_artist_event() {
|
||||
let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap();
|
||||
@@ -0,0 +1,399 @@
|
||||
use anyhow::Result;
|
||||
use log::{debug, info, warn};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::{
|
||||
AddArtistBody, AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams,
|
||||
LidarrCommandBody,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use urlencoding::encode;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_artists_network_tests.rs"]
|
||||
mod lidarr_artists_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn delete_artist(
|
||||
&mut self,
|
||||
delete_artist_params: DeleteArtistParams,
|
||||
) -> Result<()> {
|
||||
let event = LidarrEvent::DeleteArtist(DeleteArtistParams::default());
|
||||
let DeleteArtistParams {
|
||||
id,
|
||||
delete_files,
|
||||
add_import_list_exclusion,
|
||||
} = delete_artist_params;
|
||||
|
||||
info!(
|
||||
"Deleting Lidarr artist with ID: {id} with deleteFiles={delete_files} and addImportListExclusion={add_import_list_exclusion}"
|
||||
);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{id}")),
|
||||
Some(format!(
|
||||
"deleteFiles={delete_files}&addImportListExclusion={add_import_list_exclusion}"
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn list_artists(&mut self) -> Result<Vec<Artist>> {
|
||||
info!("Fetching Lidarr artists");
|
||||
let event = LidarrEvent::ListArtists;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Artist>>(request_props, |mut artists_vec, mut app| {
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _)
|
||||
) {
|
||||
artists_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.lidarr_data.artists.set_items(artists_vec);
|
||||
app.data.lidarr_data.artists.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_artist_details(
|
||||
&mut self,
|
||||
artist_id: i64,
|
||||
) -> Result<Artist> {
|
||||
info!("Fetching details for Lidarr artist with ID: {artist_id}");
|
||||
let event = LidarrEvent::GetArtistDetails(artist_id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Artist>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn toggle_artist_monitoring(
|
||||
&mut self,
|
||||
artist_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = LidarrEvent::ToggleArtistMonitoring(artist_id);
|
||||
|
||||
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
|
||||
info!("Toggling artist monitoring for artist with ID: {artist_id}");
|
||||
info!("Fetching artist details for artist with ID: {artist_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut response = String::new();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
|
||||
response = detailed_artist_body.to_string()
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Constructing toggle artist monitoring body");
|
||||
|
||||
match serde_json::from_str::<Value>(&response) {
|
||||
Ok(mut detailed_artist_body) => {
|
||||
let monitored = detailed_artist_body
|
||||
.get("monitored")
|
||||
.unwrap()
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
|
||||
*detailed_artist_body.get_mut("monitored").unwrap() = json!(!monitored);
|
||||
|
||||
debug!("Toggle artist monitoring body: {detailed_artist_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Put,
|
||||
Some(detailed_artist_body),
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Request for detailed artist body was interrupted");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn update_all_artists(&mut self) -> Result<Value> {
|
||||
info!("Updating all artists");
|
||||
let event = LidarrEvent::UpdateAllArtists;
|
||||
let body = LidarrCommandBody {
|
||||
name: "RefreshArtist".to_owned(),
|
||||
..LidarrCommandBody::default()
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<LidarrCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn update_and_scan_artist(
|
||||
&mut self,
|
||||
artist_id: i64,
|
||||
) -> Result<Value> {
|
||||
let event = LidarrEvent::UpdateAndScanArtist(artist_id);
|
||||
info!("Updating and scanning artist with ID: {artist_id}");
|
||||
let body = LidarrCommandBody {
|
||||
name: "RefreshArtist".to_owned(),
|
||||
artist_id: Some(artist_id),
|
||||
..LidarrCommandBody::default()
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<LidarrCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn trigger_automatic_artist_search(
|
||||
&mut self,
|
||||
artist_id: i64,
|
||||
) -> Result<Value> {
|
||||
let event = LidarrEvent::TriggerAutomaticArtistSearch(artist_id);
|
||||
info!("Searching indexers for artist with ID: {artist_id}");
|
||||
let body = LidarrCommandBody {
|
||||
name: "ArtistSearch".to_owned(),
|
||||
artist_id: Some(artist_id),
|
||||
..LidarrCommandBody::default()
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<LidarrCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn search_artist(
|
||||
&mut self,
|
||||
query: String,
|
||||
) -> Result<Vec<AddArtistSearchResult>> {
|
||||
info!("Searching for artist: {query}");
|
||||
let event = LidarrEvent::SearchNewArtist(String::new());
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("term={}", encode(&query))),
|
||||
)
|
||||
.await;
|
||||
|
||||
let result = self
|
||||
.handle_request::<(), Vec<AddArtistSearchResult>>(request_props, |artist_vec, mut app| {
|
||||
if artist_vec.is_empty() {
|
||||
app.pop_and_push_navigation_stack(ActiveLidarrBlock::AddArtistEmptySearchResults.into());
|
||||
} else if let Some(add_searched_artists) =
|
||||
app.data.lidarr_data.add_searched_artists.as_mut()
|
||||
{
|
||||
add_searched_artists.set_items(artist_vec);
|
||||
} else {
|
||||
let mut add_searched_artists = StatefulTable::default();
|
||||
add_searched_artists.set_items(artist_vec);
|
||||
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
self.app.lock().await.data.lidarr_data.add_searched_artists = Some(StatefulTable::default());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn add_artist(
|
||||
&mut self,
|
||||
mut add_artist_body: AddArtistBody,
|
||||
) -> Result<Value> {
|
||||
info!("Adding Lidarr artist: {}", add_artist_body.artist_name);
|
||||
if let Some(tag_input_str) = add_artist_body.tag_input_string.as_ref() {
|
||||
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
|
||||
add_artist_body.tags = tag_ids_vec;
|
||||
}
|
||||
let event = LidarrEvent::AddArtist(AddArtistBody::default());
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Post,
|
||||
Some(add_artist_body),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<AddArtistBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn edit_artist(
|
||||
&mut self,
|
||||
mut edit_artist_params: EditArtistParams,
|
||||
) -> Result<()> {
|
||||
info!("Editing Lidarr artist");
|
||||
if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() {
|
||||
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
|
||||
edit_artist_params.tags = Some(tag_ids_vec);
|
||||
}
|
||||
let artist_id = edit_artist_params.artist_id;
|
||||
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
|
||||
let event = LidarrEvent::EditArtist(EditArtistParams::default());
|
||||
info!("Fetching artist details for artist with ID: {artist_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut response = String::new();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
|
||||
response = detailed_artist_body.to_string()
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Constructing edit artist body");
|
||||
|
||||
let mut detailed_artist_body: Value = serde_json::from_str(&response)?;
|
||||
let (
|
||||
monitored,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tags,
|
||||
) = {
|
||||
let monitored = edit_artist_params.monitored.unwrap_or(
|
||||
detailed_artist_body["monitored"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'monitored'"),
|
||||
);
|
||||
let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| {
|
||||
serde_json::from_value(detailed_artist_body["monitorNewItems"].clone())
|
||||
.expect("Unable to deserialize 'monitorNewItems'")
|
||||
});
|
||||
let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| {
|
||||
detailed_artist_body["qualityProfileId"]
|
||||
.as_i64()
|
||||
.expect("Unable to deserialize 'qualityProfileId'")
|
||||
});
|
||||
let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| {
|
||||
detailed_artist_body["metadataProfileId"]
|
||||
.as_i64()
|
||||
.expect("Unable to deserialize 'metadataProfileId'")
|
||||
});
|
||||
let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| {
|
||||
detailed_artist_body["path"]
|
||||
.as_str()
|
||||
.expect("Unable to deserialize 'path'")
|
||||
.to_owned()
|
||||
});
|
||||
let tags = if edit_artist_params.clear_tags {
|
||||
vec![]
|
||||
} else {
|
||||
edit_artist_params.tags.unwrap_or(
|
||||
detailed_artist_body["tags"]
|
||||
.as_array()
|
||||
.expect("Unable to deserialize 'tags'")
|
||||
.iter()
|
||||
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
(
|
||||
monitored,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tags,
|
||||
)
|
||||
};
|
||||
|
||||
*detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored);
|
||||
*detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items);
|
||||
*detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
|
||||
*detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id);
|
||||
*detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path);
|
||||
*detailed_artist_body.get_mut("tags").unwrap() = json!(tags);
|
||||
|
||||
debug!("Edit artist body: {detailed_artist_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Put,
|
||||
Some(detailed_artist_body),
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,356 +1,2 @@
|
||||
use anyhow::Result;
|
||||
use log::{debug, info, warn};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::{
|
||||
AddArtistBody, AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_models::CommandBody;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use urlencoding::encode;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_library_network_tests.rs"]
|
||||
mod lidarr_library_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn delete_artist(
|
||||
&mut self,
|
||||
delete_artist_params: DeleteArtistParams,
|
||||
) -> Result<()> {
|
||||
let event = LidarrEvent::DeleteArtist(DeleteArtistParams::default());
|
||||
let DeleteArtistParams {
|
||||
id,
|
||||
delete_files,
|
||||
add_import_list_exclusion,
|
||||
} = delete_artist_params;
|
||||
|
||||
info!(
|
||||
"Deleting Lidarr artist with ID: {id} with deleteFiles={delete_files} and addImportListExclusion={add_import_list_exclusion}"
|
||||
);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{id}")),
|
||||
Some(format!(
|
||||
"deleteFiles={delete_files}&addImportListExclusion={add_import_list_exclusion}"
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn list_artists(&mut self) -> Result<Vec<Artist>> {
|
||||
info!("Fetching Lidarr artists");
|
||||
let event = LidarrEvent::ListArtists;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Artist>>(request_props, |mut artists_vec, mut app| {
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _)
|
||||
) {
|
||||
artists_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.lidarr_data.artists.set_items(artists_vec);
|
||||
app.data.lidarr_data.artists.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_artist_details(
|
||||
&mut self,
|
||||
artist_id: i64,
|
||||
) -> Result<Artist> {
|
||||
info!("Fetching details for Lidarr artist with ID: {artist_id}");
|
||||
let event = LidarrEvent::GetArtistDetails(artist_id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Artist>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn toggle_artist_monitoring(
|
||||
&mut self,
|
||||
artist_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = LidarrEvent::ToggleArtistMonitoring(artist_id);
|
||||
|
||||
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
|
||||
info!("Toggling artist monitoring for artist with ID: {artist_id}");
|
||||
info!("Fetching artist details for artist with ID: {artist_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut response = String::new();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
|
||||
response = detailed_artist_body.to_string()
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Constructing toggle artist monitoring body");
|
||||
|
||||
match serde_json::from_str::<Value>(&response) {
|
||||
Ok(mut detailed_artist_body) => {
|
||||
let monitored = detailed_artist_body
|
||||
.get("monitored")
|
||||
.unwrap()
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
|
||||
*detailed_artist_body.get_mut("monitored").unwrap() = json!(!monitored);
|
||||
|
||||
debug!("Toggle artist monitoring body: {detailed_artist_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Put,
|
||||
Some(detailed_artist_body),
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Request for detailed artist body was interrupted");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn update_all_artists(&mut self) -> Result<Value> {
|
||||
info!("Updating all artists");
|
||||
let event = LidarrEvent::UpdateAllArtists;
|
||||
let body = CommandBody {
|
||||
name: "RefreshArtist".to_owned(),
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn search_artist(
|
||||
&mut self,
|
||||
query: String,
|
||||
) -> Result<Vec<AddArtistSearchResult>> {
|
||||
info!("Searching for artist: {query}");
|
||||
let event = LidarrEvent::SearchNewArtist(String::new());
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("term={}", encode(&query))),
|
||||
)
|
||||
.await;
|
||||
|
||||
let result = self
|
||||
.handle_request::<(), Vec<AddArtistSearchResult>>(request_props, |artist_vec, mut app| {
|
||||
if artist_vec.is_empty() {
|
||||
app.pop_and_push_navigation_stack(ActiveLidarrBlock::AddArtistEmptySearchResults.into());
|
||||
} else if let Some(add_searched_artists) =
|
||||
app.data.lidarr_data.add_searched_artists.as_mut()
|
||||
{
|
||||
add_searched_artists.set_items(artist_vec);
|
||||
} else {
|
||||
let mut add_searched_artists = StatefulTable::default();
|
||||
add_searched_artists.set_items(artist_vec);
|
||||
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
self.app.lock().await.data.lidarr_data.add_searched_artists = Some(StatefulTable::default());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn add_artist(
|
||||
&mut self,
|
||||
mut add_artist_body: AddArtistBody,
|
||||
) -> Result<Value> {
|
||||
info!("Adding Lidarr artist: {}", add_artist_body.artist_name);
|
||||
if let Some(tag_input_str) = add_artist_body.tag_input_string.as_ref() {
|
||||
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
|
||||
add_artist_body.tags = tag_ids_vec;
|
||||
}
|
||||
let event = LidarrEvent::AddArtist(AddArtistBody::default());
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Post,
|
||||
Some(add_artist_body),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<AddArtistBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn edit_artist(
|
||||
&mut self,
|
||||
mut edit_artist_params: EditArtistParams,
|
||||
) -> Result<()> {
|
||||
info!("Editing Lidarr artist");
|
||||
if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() {
|
||||
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
|
||||
edit_artist_params.tags = Some(tag_ids_vec);
|
||||
}
|
||||
let artist_id = edit_artist_params.artist_id;
|
||||
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
|
||||
let event = LidarrEvent::EditArtist(EditArtistParams::default());
|
||||
info!("Fetching artist details for artist with ID: {artist_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut response = String::new();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
|
||||
response = detailed_artist_body.to_string()
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Constructing edit artist body");
|
||||
|
||||
let mut detailed_artist_body: Value = serde_json::from_str(&response)?;
|
||||
let (
|
||||
monitored,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tags,
|
||||
) = {
|
||||
let monitored = edit_artist_params.monitored.unwrap_or(
|
||||
detailed_artist_body["monitored"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'monitored'"),
|
||||
);
|
||||
let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| {
|
||||
serde_json::from_value(detailed_artist_body["monitorNewItems"].clone())
|
||||
.expect("Unable to deserialize 'monitorNewItems'")
|
||||
});
|
||||
let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| {
|
||||
detailed_artist_body["qualityProfileId"]
|
||||
.as_i64()
|
||||
.expect("Unable to deserialize 'qualityProfileId'")
|
||||
});
|
||||
let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| {
|
||||
detailed_artist_body["metadataProfileId"]
|
||||
.as_i64()
|
||||
.expect("Unable to deserialize 'metadataProfileId'")
|
||||
});
|
||||
let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| {
|
||||
detailed_artist_body["path"]
|
||||
.as_str()
|
||||
.expect("Unable to deserialize 'path'")
|
||||
.to_owned()
|
||||
});
|
||||
let tags = if edit_artist_params.clear_tags {
|
||||
vec![]
|
||||
} else {
|
||||
edit_artist_params.tags.unwrap_or(
|
||||
detailed_artist_body["tags"]
|
||||
.as_array()
|
||||
.expect("Unable to deserialize 'tags'")
|
||||
.iter()
|
||||
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
(
|
||||
monitored,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tags,
|
||||
)
|
||||
};
|
||||
|
||||
*detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored);
|
||||
*detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items);
|
||||
*detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
|
||||
*detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id);
|
||||
*detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path);
|
||||
*detailed_artist_body.get_mut("tags").unwrap() = json!(tags);
|
||||
|
||||
debug!("Edit artist body: {detailed_artist_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Put,
|
||||
Some(detailed_artist_body),
|
||||
Some(format!("/{artist_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
mod albums;
|
||||
mod artists;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
pub mod test_utils {
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::lidarr_models::{
|
||||
AddArtistSearchResult, Artist, ArtistStatistics, ArtistStatus, DownloadRecord, DownloadStatus,
|
||||
DownloadsResponse, EditArtistParams, Member, MetadataProfile, NewItemMonitorType, Ratings,
|
||||
SystemStatus,
|
||||
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
|
||||
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, Member, MetadataProfile,
|
||||
NewItemMonitorType, Ratings, SystemStatus,
|
||||
};
|
||||
use crate::models::servarr_models::{QualityProfile, RootFolder, Tag};
|
||||
use bimap::BiMap;
|
||||
@@ -50,6 +50,27 @@ pub mod test_utils {
|
||||
"percentOfTracks": 99.9
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const ALBUM_JSON: &str = r#"{
|
||||
"id": 1,
|
||||
"title": "Test Album",
|
||||
"foreignAlbumId": "test-foreign-album-id",
|
||||
"monitored": true,
|
||||
"anyReleaseOk": true,
|
||||
"profileId": 1,
|
||||
"duration": 180,
|
||||
"albumType": "Album",
|
||||
"genres": ["Classical"],
|
||||
"ratings": {"votes": 15, "value": 8.4},
|
||||
"releaseDate": "2023-01-01T00:00:00Z",
|
||||
"statistics": {
|
||||
"trackFileCount": 10,
|
||||
"trackCount": 10,
|
||||
"totalTrackCount": 10,
|
||||
"sizeOnDisk": 1024,
|
||||
"percentOfTracks": 99.9
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub fn member() -> Member {
|
||||
Member {
|
||||
@@ -199,4 +220,33 @@ pub mod test_utils {
|
||||
ratings: Some(ratings()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn album_statistics() -> AlbumStatistics {
|
||||
AlbumStatistics {
|
||||
track_file_count: 10,
|
||||
track_count: 10,
|
||||
total_track_count: 10,
|
||||
size_on_disk: 1024,
|
||||
percent_of_tracks: 99.9,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn album() -> Album {
|
||||
Album {
|
||||
id: 1,
|
||||
title: "Test Album".into(),
|
||||
foreign_album_id: "test-foreign-album-id".to_string(),
|
||||
monitored: true,
|
||||
any_release_ok: true,
|
||||
profile_id: 1,
|
||||
duration: 180,
|
||||
album_type: Some("Album".to_owned()),
|
||||
genres: vec!["Classical".to_owned()],
|
||||
ratings: Some(ratings()),
|
||||
release_date: Some(DateTime::from(
|
||||
DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap(),
|
||||
)),
|
||||
statistics: Some(album_statistics()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_command(#[values(LidarrEvent::UpdateAllArtists)] event: LidarrEvent) {
|
||||
fn test_resource_command(
|
||||
#[values(
|
||||
LidarrEvent::UpdateAllArtists,
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(0),
|
||||
LidarrEvent::UpdateAndScanArtist(0)
|
||||
)]
|
||||
event: LidarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/command");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_albums(
|
||||
#[values(LidarrEvent::GetAlbums(0), LidarrEvent::ToggleAlbumMonitoring(0), LidarrEvent::GetAlbumDetails(0))] event: LidarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/album");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
|
||||
#[case(LidarrEvent::GetDownloads(500), "/queue")]
|
||||
|
||||
@@ -28,6 +28,8 @@ pub enum LidarrEvent {
|
||||
DeleteArtist(DeleteArtistParams),
|
||||
DeleteTag(i64),
|
||||
EditArtist(EditArtistParams),
|
||||
GetAlbums(i64),
|
||||
GetAlbumDetails(i64),
|
||||
GetArtistDetails(i64),
|
||||
GetDiskSpace,
|
||||
GetDownloads(u64),
|
||||
@@ -41,8 +43,11 @@ pub enum LidarrEvent {
|
||||
HealthCheck,
|
||||
ListArtists,
|
||||
SearchNewArtist(String),
|
||||
ToggleAlbumMonitoring(i64),
|
||||
ToggleArtistMonitoring(i64),
|
||||
TriggerAutomaticArtistSearch(i64),
|
||||
UpdateAllArtists,
|
||||
UpdateAndScanArtist(i64),
|
||||
}
|
||||
|
||||
impl NetworkResource for LidarrEvent {
|
||||
@@ -55,10 +60,13 @@ impl NetworkResource for LidarrEvent {
|
||||
| LidarrEvent::ListArtists
|
||||
| LidarrEvent::AddArtist(_)
|
||||
| LidarrEvent::ToggleArtistMonitoring(_) => "/artist",
|
||||
LidarrEvent::GetAlbums(_) | LidarrEvent::ToggleAlbumMonitoring(_) | LidarrEvent::GetAlbumDetails(_) => "/album",
|
||||
LidarrEvent::GetDiskSpace => "/diskspace",
|
||||
LidarrEvent::GetDownloads(_) => "/queue",
|
||||
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
|
||||
LidarrEvent::UpdateAllArtists => "/command",
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(_)
|
||||
| LidarrEvent::UpdateAllArtists
|
||||
| LidarrEvent::UpdateAndScanArtist(_) => "/command",
|
||||
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
|
||||
LidarrEvent::GetQualityProfiles => "/qualityprofile",
|
||||
LidarrEvent::GetRootFolders => "/rootfolder",
|
||||
@@ -89,10 +97,14 @@ impl Network<'_, '_> {
|
||||
.delete_lidarr_tag(tag_id)
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetAlbums(artist_id) => {
|
||||
self.get_albums(artist_id).await.map(LidarrSerdeable::from)
|
||||
}
|
||||
LidarrEvent::GetArtistDetails(artist_id) => self
|
||||
.get_artist_details(artist_id)
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetAlbumDetails(album_id) => self.get_album_details(album_id).await.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetDownloads(count) => self
|
||||
.get_lidarr_downloads(count)
|
||||
@@ -128,11 +140,23 @@ impl Network<'_, '_> {
|
||||
LidarrEvent::SearchNewArtist(query) => {
|
||||
self.search_artist(query).await.map(LidarrSerdeable::from)
|
||||
}
|
||||
LidarrEvent::ToggleAlbumMonitoring(album_id) => self
|
||||
.toggle_album_monitoring(album_id)
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::ToggleArtistMonitoring(artist_id) => self
|
||||
.toggle_artist_monitoring(artist_id)
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(artist_id) => self
|
||||
.trigger_automatic_artist_search(artist_id)
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from),
|
||||
LidarrEvent::UpdateAndScanArtist(artist_id) => self
|
||||
.update_and_scan_artist(artist_id)
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from),
|
||||
LidarrEvent::AddArtist(body) => self.add_artist(body).await.map(LidarrSerdeable::from),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
use chrono::Utc;
|
||||
use deunicode::deunicode;
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::Album;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock};
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::utils::{
|
||||
borderless_block, get_width_from_percentage, layout_block_top_border, title_block,
|
||||
};
|
||||
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
|
||||
use crate::ui::widgets::managarr_table::ManagarrTable;
|
||||
use crate::ui::widgets::popup::{Popup, Size};
|
||||
use crate::ui::{DrawUi, draw_popup, draw_tabs};
|
||||
use crate::utils::convert_to_gb;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "artist_details_ui_tests.rs"]
|
||||
mod artist_details_ui_tests;
|
||||
|
||||
pub(super) struct ArtistDetailsUi;
|
||||
|
||||
impl DrawUi for ArtistDetailsUi {
|
||||
fn accepts(route: Route) -> bool {
|
||||
let Route::Lidarr(active_lidarr_block, _) = route else {
|
||||
return false;
|
||||
};
|
||||
ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block)
|
||||
}
|
||||
|
||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
|
||||
let route = app.get_current_route();
|
||||
if let Route::Lidarr(active_lidarr_block, _) = route {
|
||||
let draw_artist_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| {
|
||||
f.render_widget(
|
||||
title_block(
|
||||
&app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.current_selection()
|
||||
.artist_name
|
||||
.text,
|
||||
),
|
||||
popup_area,
|
||||
);
|
||||
let [description_area, detail_area] =
|
||||
Layout::vertical([Constraint::Percentage(37), Constraint::Fill(0)])
|
||||
.margin(1)
|
||||
.areas(popup_area);
|
||||
draw_artist_description(f, app, description_area);
|
||||
let content_area = draw_tabs(
|
||||
f,
|
||||
detail_area,
|
||||
"Artist Details",
|
||||
&app.data.lidarr_data.artist_info_tabs,
|
||||
);
|
||||
draw_artist_details(f, app, content_area);
|
||||
|
||||
match active_lidarr_block {
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
let prompt = format!(
|
||||
"Do you want to trigger an automatic search of your indexers for all monitored album(s) for the artist: {}?",
|
||||
app.data.lidarr_data.artists.current_selection().artist_name
|
||||
);
|
||||
let confirmation_prompt = ConfirmationPrompt::new()
|
||||
.title("Automatic Artist Search")
|
||||
.prompt(&prompt)
|
||||
.yes_no_value(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
f.render_widget(
|
||||
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
|
||||
f.area(),
|
||||
);
|
||||
}
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
|
||||
let prompt = format!(
|
||||
"Do you want to trigger an update and disk scan for the artist: {}?",
|
||||
app.data.lidarr_data.artists.current_selection().artist_name
|
||||
);
|
||||
let confirmation_prompt = ConfirmationPrompt::new()
|
||||
.title("Update and Scan")
|
||||
.prompt(&prompt)
|
||||
.yes_no_value(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
f.render_widget(
|
||||
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
|
||||
f.area(),
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
};
|
||||
|
||||
draw_popup(f, app, draw_artist_details_popup, Size::XXLarge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_artist_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
let current_selection = app.data.lidarr_data.artists.current_selection();
|
||||
let monitored = if current_selection.monitored {
|
||||
"Yes"
|
||||
} else {
|
||||
"No"
|
||||
};
|
||||
let quality_profile = app
|
||||
.data
|
||||
.lidarr_data
|
||||
.quality_profile_map
|
||||
.get_by_left(¤t_selection.quality_profile_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let metadata_profile = app
|
||||
.data
|
||||
.lidarr_data
|
||||
.metadata_profile_map
|
||||
.get_by_left(¤t_selection.metadata_profile_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let overview = Regex::new(r"[\r\n\t]")
|
||||
.unwrap()
|
||||
.replace_all(
|
||||
&deunicode(
|
||||
current_selection
|
||||
.overview
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
"",
|
||||
)
|
||||
.to_string();
|
||||
|
||||
let mut artist_description = vec![
|
||||
Line::from(vec![
|
||||
"Artist: ".primary().bold(),
|
||||
current_selection.artist_name.text.clone().primary().bold(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Overview: ".primary().bold(),
|
||||
overview.default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Disambiguation: ".primary().bold(),
|
||||
current_selection
|
||||
.disambiguation
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Type: ".primary().bold(),
|
||||
current_selection
|
||||
.artist_type
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Status: ".primary().bold(),
|
||||
current_selection.status.to_display_str().default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Genres: ".primary().bold(),
|
||||
current_selection.genres.join(", ").default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Rating: ".primary().bold(),
|
||||
current_selection
|
||||
.ratings
|
||||
.as_ref()
|
||||
.map_or_else(
|
||||
|| "N/A".to_owned(),
|
||||
|r| format!("{}%", (r.value * 10.0) as i32),
|
||||
)
|
||||
.default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Path: ".primary().bold(),
|
||||
current_selection.path.clone().default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Quality Profile: ".primary().bold(),
|
||||
quality_profile.default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Metadata Profile: ".primary().bold(),
|
||||
metadata_profile.default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Monitored: ".primary().bold(),
|
||||
monitored.default_color(),
|
||||
]),
|
||||
];
|
||||
|
||||
if let Some(stats) = current_selection.statistics.as_ref() {
|
||||
let size = convert_to_gb(stats.size_on_disk);
|
||||
artist_description.extend(vec![
|
||||
Line::from(vec![
|
||||
"Albums: ".primary().bold(),
|
||||
stats.album_count.to_string().default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Tracks: ".primary().bold(),
|
||||
format!("{}/{}", stats.track_file_count, stats.total_track_count).default_color(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Size on Disk: ".primary().bold(),
|
||||
format!("{size:.2} GB").default_color(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
let description_paragraph = Paragraph::new(artist_description)
|
||||
.block(borderless_block())
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(description_paragraph, area);
|
||||
}
|
||||
|
||||
fn draw_artist_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
if let Route::Lidarr(active_lidarr_block, _) =
|
||||
app.data.lidarr_data.artist_info_tabs.get_active_route()
|
||||
{
|
||||
match active_lidarr_block {
|
||||
ActiveLidarrBlock::ArtistDetails => draw_albums_table(f, app, area),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
|
||||
let current_selection = if app.data.lidarr_data.albums.is_empty() {
|
||||
Album::default()
|
||||
} else {
|
||||
app.data.lidarr_data.albums.current_selection().clone()
|
||||
};
|
||||
let content = Some(&mut app.data.lidarr_data.albums);
|
||||
let album_row_mapping = |album: &Album| {
|
||||
album.title.scroll_left_or_reset(
|
||||
get_width_from_percentage(area, 33),
|
||||
*album == current_selection,
|
||||
app.ui_scroll_tick_count == 0,
|
||||
);
|
||||
let monitored = if album.monitored { "🏷" } else { "" };
|
||||
let album_type = album.album_type.clone().unwrap_or_default();
|
||||
let release_date = album
|
||||
.release_date
|
||||
.map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string());
|
||||
let track_count = album.statistics.as_ref().map_or_else(
|
||||
|| "0/0".to_owned(),
|
||||
|s| format!("{}/{}", s.track_file_count, s.total_track_count),
|
||||
);
|
||||
let size = album
|
||||
.statistics
|
||||
.as_ref()
|
||||
.map_or(0f64, |s| convert_to_gb(s.size_on_disk));
|
||||
let duration_mins = album.duration / 60000;
|
||||
|
||||
let row = Row::new(vec![
|
||||
Cell::from(monitored.to_owned()),
|
||||
Cell::from(album.title.to_string()),
|
||||
Cell::from(album_type),
|
||||
Cell::from(track_count),
|
||||
Cell::from(format!("{duration_mins} min")),
|
||||
Cell::from(release_date),
|
||||
Cell::from(format!("{size:.2} GB")),
|
||||
]);
|
||||
|
||||
if !album.monitored {
|
||||
row.unmonitored()
|
||||
} else if let Some(stats) = album.statistics.as_ref() {
|
||||
if stats.track_file_count == stats.total_track_count && stats.total_track_count > 0 {
|
||||
row.downloaded()
|
||||
} else if let Some(release_date) = album.release_date.as_ref() {
|
||||
if release_date > &Utc::now() {
|
||||
row.unreleased()
|
||||
} else {
|
||||
row.missing()
|
||||
}
|
||||
} else {
|
||||
row.missing()
|
||||
}
|
||||
} else {
|
||||
row.indeterminate()
|
||||
}
|
||||
};
|
||||
|
||||
let is_searching = active_lidarr_block == ActiveLidarrBlock::SearchAlbums;
|
||||
let album_table = ManagarrTable::new(content, album_row_mapping)
|
||||
.block(layout_block_top_border())
|
||||
.loading(app.is_loading)
|
||||
.searching(is_searching)
|
||||
.search_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::SearchAlbumsError)
|
||||
.headers([
|
||||
"Monitored",
|
||||
"Title",
|
||||
"Type",
|
||||
"Tracks",
|
||||
"Duration",
|
||||
"Release Date",
|
||||
"Size",
|
||||
])
|
||||
.constraints([
|
||||
Constraint::Percentage(7),
|
||||
Constraint::Percentage(35),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(13),
|
||||
Constraint::Percentage(15),
|
||||
]);
|
||||
|
||||
if is_searching {
|
||||
album_table.show_cursor(f, area);
|
||||
}
|
||||
|
||||
f.render_widget(album_table, area);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
|
||||
};
|
||||
use crate::ui::DrawUi;
|
||||
use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi;
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_ui_accepts() {
|
||||
let mut blocks = ARTIST_DETAILS_BLOCKS.clone().to_vec();
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if blocks.contains(&active_lidarr_block) {
|
||||
assert!(ArtistDetailsUi::accepts(active_lidarr_block.into()));
|
||||
} else {
|
||||
assert!(!ArtistDetailsUi::accepts(active_lidarr_block.into()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mod snapshot_tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::ui::DrawUi;
|
||||
use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi;
|
||||
use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app};
|
||||
|
||||
#[rstest]
|
||||
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
|
||||
#[case(ActiveLidarrBlock::SearchAlbums, 0)]
|
||||
#[case(ActiveLidarrBlock::SearchAlbumsError, 0)]
|
||||
#[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, 0)]
|
||||
#[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, 0)]
|
||||
fn test_artist_details_ui_renders(
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] index: usize,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.data.lidarr_data.artist_info_tabs.set_index(index);
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
ArtistDetailsUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(
|
||||
format!("artist_details_{active_lidarr_block}_{index}"),
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
|
||||
fn test_artist_details_ui_renders_artist_details_loading(
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] index: usize,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.data.lidarr_data.artist_info_tabs.set_index(index);
|
||||
app.is_loading = true;
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
ArtistDetailsUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(
|
||||
format!("loading_artist_details_{active_lidarr_block}"),
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
|
||||
fn test_artist_details_ui_renders_artist_details_empty(
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] index: usize,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.albums = StatefulTable::default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.data.lidarr_data.artist_info_tabs.set_index(index);
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
ArtistDetailsUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(
|
||||
format!("empty_artist_details_{active_lidarr_block}"),
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_ui_renders_update_and_scan_prompt_over_artist_details() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
ArtistDetailsUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_ui_renders_automatic_search_prompt_over_artist_details() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into());
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
ArtistDetailsUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,13 @@ use ratatui::widgets::ListItem;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||
use crate::render_selectable_input_box;
|
||||
|
||||
use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi;
|
||||
use crate::ui::utils::title_block_centered;
|
||||
use crate::ui::widgets::button::Button;
|
||||
use crate::ui::widgets::checkbox::Checkbox;
|
||||
@@ -34,7 +37,13 @@ impl DrawUi for EditArtistUi {
|
||||
}
|
||||
|
||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
|
||||
if let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() {
|
||||
if let Route::Lidarr(active_lidarr_block, context_option) = app.get_current_route() {
|
||||
if let Some(context) = context_option
|
||||
&& ARTIST_DETAILS_BLOCKS.contains(&context)
|
||||
{
|
||||
draw_popup(f, app, ArtistDetailsUi::draw, Size::Large);
|
||||
}
|
||||
|
||||
let draw_edit_artist_prompt = |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| {
|
||||
draw_edit_artist_confirmation_prompt(f, app, prompt_area);
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ mod tests {
|
||||
|
||||
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS,
|
||||
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ARTIST_BLOCKS,
|
||||
EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS,
|
||||
};
|
||||
use crate::ui::DrawUi;
|
||||
use crate::ui::lidarr_ui::library::{LibraryUi, decorate_artist_row_with_style};
|
||||
@@ -19,6 +20,7 @@ mod tests {
|
||||
library_ui_blocks.extend(DELETE_ARTIST_BLOCKS);
|
||||
library_ui_blocks.extend(EDIT_ARTIST_BLOCKS);
|
||||
library_ui_blocks.extend(ADD_ARTIST_BLOCKS);
|
||||
library_ui_blocks.extend(ARTIST_DETAILS_BLOCKS);
|
||||
|
||||
for active_lidarr_block in ActiveLidarrBlock::iter() {
|
||||
if library_ui_blocks.contains(&active_lidarr_block) {
|
||||
@@ -182,7 +184,7 @@ mod tests {
|
||||
use crate::app::App;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS,
|
||||
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
@@ -263,5 +265,49 @@ mod tests {
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_ui_renders_artist_details_over_library() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
LibraryUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_ui_renders_edit_artist_over_artist_details() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
LibraryUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_ui_renders_dropdown_over_edit_artist_over_artist_details() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::EditArtistSelectMetadataProfile.into());
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
|
||||
|
||||
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||
LibraryUi::draw(f, app, f.area());
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use add_artist_ui::AddArtistUi;
|
||||
use artist_details_ui::ArtistDetailsUi;
|
||||
use delete_artist_ui::DeleteArtistUi;
|
||||
use edit_artist_ui::EditArtistUi;
|
||||
use ratatui::{
|
||||
@@ -28,6 +29,7 @@ use crate::{
|
||||
};
|
||||
|
||||
mod add_artist_ui;
|
||||
mod artist_details_ui;
|
||||
mod delete_artist_ui;
|
||||
mod edit_artist_ui;
|
||||
|
||||
@@ -43,6 +45,7 @@ impl DrawUi for LibraryUi {
|
||||
return AddArtistUi::accepts(route)
|
||||
|| DeleteArtistUi::accepts(route)
|
||||
|| EditArtistUi::accepts(route)
|
||||
|| ArtistDetailsUi::accepts(route)
|
||||
|| LIBRARY_BLOCKS.contains(&active_lidarr_block);
|
||||
}
|
||||
|
||||
@@ -57,6 +60,7 @@ impl DrawUi for LibraryUi {
|
||||
_ if AddArtistUi::accepts(route) => AddArtistUi::draw(f, app, area),
|
||||
_ if DeleteArtistUi::accepts(route) => DeleteArtistUi::draw(f, app, area),
|
||||
_ if EditArtistUi::accepts(route) => EditArtistUi::draw(f, app, area),
|
||||
_ if ArtistDetailsUi::accepts(route) => ArtistDetailsUi::draw(f, app, area),
|
||||
Route::Lidarr(ActiveLidarrBlock::UpdateAllArtistsPrompt, _) => {
|
||||
let confirmation_prompt = ConfirmationPrompt::new()
|
||||
.title("Update All Artists")
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮
|
||||
│Test Artist │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ ╭─────────────── Error ───────────────╮ │
|
||||
│ │ No artists found matching your query! │ │
|
||||
│ │ │ │
|
||||
│ ╰───────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮
|
||||
│Test Artist │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮
|
||||
│Test Artist │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✔ Name Type Status Rating Genres │
|
||||
│=> Test Artist Person Continuing 8.4 soundtrack │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✔ Name Type Status Rating Genres │
|
||||
│=> Test Artist Person Continuing 8.4 soundtrack │
|
||||
│=> ✔ Test Artist Person Continuing 8.4 soundtrack │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰──────│ │───────╯
|
||||
╭──────│ │───────╮
|
||||
│ ✔ │ │ │
|
||||
│=> │ │ │
|
||||
│=> ✔ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ╭─────────────────────────────────────────────────╮ │ │
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰──────│ │───────╯
|
||||
╭──────│ │───────╮
|
||||
│ ✔ │ │ │
|
||||
│=> │ │ │
|
||||
│=> ✔ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ╭─────────────────────────────────────────────────╮ │ │
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰──────│ │───────╯
|
||||
╭──────│ │───────╮
|
||||
│ ✔ │ │ │
|
||||
│=> │ │ │
|
||||
│=> ✔ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ╭─────────────────────────────────────────────────╮ │ │
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰──────│ │───────╯
|
||||
╭──────│ │───────╮
|
||||
│ ✔ │ │ │
|
||||
│=> │ │ │
|
||||
│=> ✔ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ╭─────────────────────────────────────────────────╮ │ │
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰──────│ │───────╯
|
||||
╭──────│ │───────╮
|
||||
│ ✔ │ │ │
|
||||
│=> │ │ │
|
||||
│=> ✔ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ╭─────────────────────────────────────────────────╮ │ │
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰──────│ │───────╯
|
||||
╭──────│ │───────╮
|
||||
│ ✔ │ │ │
|
||||
│=> │ │ │
|
||||
│=> ✔ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ╭─────────────────────────────────────────────────╮ │ │
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰──────│ │───────╯
|
||||
╭──────│ │───────╮
|
||||
│ ✔ │ │ │
|
||||
│=> │ │ │
|
||||
│=> ✔ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ╭─────────────────────────────────────────────────╮ │ │
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: output
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✔ Name Type Status Rating Genres │
|
||||
│=> Test Artist Person Continuing 8.4 soundtrack │
|
||||
│=> ✔ Test Artist Person Continuing 8.4 soundtrack │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 │
|
||||
│Size on Disk: 0.00 GB │
|
||||
│ │
|
||||
│ │
|
||||
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||
││ Albums ││
|
||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │
|
||||
│Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │
|
||||
│ │ for all monitored album(s) for the artist: Alex? │ │
|
||||
│ │ │ │
|
||||
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
|
||||
││ Albums │ │ ││
|
||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||
││ Monitored Title │ │ Release Date Size ││
|
||||
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
|
||||
││ ││ Yes ││ No ││ ││
|
||||
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
|
||||
││ ╰───────────────────────────────────────────────────────────╯ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 │
|
||||
│Size on Disk: 0.00 GB │
|
||||
│ │
|
||||
│ │
|
||||
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||
││ Albums ││
|
||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ╭──────────── Error ─────────────╮ ││
|
||||
││ │ No items found matching search │ ││
|
||||
││ ╰──────────────────────────────────╯ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 │
|
||||
│Size on Disk: 0.00 GB │
|
||||
│ │
|
||||
│ │
|
||||
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||
││ Albums ││
|
||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ╭────────────── Search ───────────────╮ ││
|
||||
││ │album search │ ││
|
||||
││ ╰───────────────────────────────────────╯ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 ╭──────────────────── Update and Scan ────────────────────╮ │
|
||||
│Size on Disk: 0.00 GB │ Do you want to trigger an update and disk scan for the │ │
|
||||
│ │ artist: Alex? │ │
|
||||
│ │ │ │
|
||||
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
|
||||
││ Albums │ │ ││
|
||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||
││ Monitored Title │ │ Release Date Size ││
|
||||
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
|
||||
││ ││ Yes ││ No ││ ││
|
||||
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
|
||||
││ ╰───────────────────────────────────────────────────────────╯ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │
|
||||
│Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │
|
||||
│ │ for all monitored album(s) for the artist: Alex? │ │
|
||||
│ │ │ │
|
||||
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
|
||||
││ Albums │ │ ││
|
||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||
││ Monitored Title │ │ Release Date Size ││
|
||||
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
|
||||
││ ││ Yes ││ No ││ ││
|
||||
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
|
||||
││ ╰───────────────────────────────────────────────────────────╯ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 ╭──────────────────── Update and Scan ────────────────────╮ │
|
||||
│Size on Disk: 0.00 GB │ Do you want to trigger an update and disk scan for the │ │
|
||||
│ │ artist: Alex? │ │
|
||||
│ │ │ │
|
||||
│╭ Artist Details ───────────────────────│ │───────────────────────────────────────────╮│
|
||||
││ Albums │ │ ││
|
||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||
││ Monitored Title │ │ Release Date Size ││
|
||||
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │ │ ││
|
||||
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
|
||||
││ ││ Yes ││ No ││ ││
|
||||
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
|
||||
││ ╰───────────────────────────────────────────────────────────╯ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 │
|
||||
│Size on Disk: 0.00 GB │
|
||||
│ │
|
||||
│ │
|
||||
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||
││ Albums ││
|
||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 │
|
||||
│Size on Disk: 0.00 GB │
|
||||
│ │
|
||||
│ │
|
||||
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||
││ Albums ││
|
||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||
││ ││
|
||||
││ ││
|
||||
││ Loading ... ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+1
-1
@@ -20,7 +20,7 @@ expression: output
|
||||
|
||||
╭───────────────────── Delete Artist ─────────────────────╮
|
||||
│ Do you really want to delete the artist: │
|
||||
│ ? │
|
||||
│ Alex? │
|
||||
│ │
|
||||
│ │
|
||||
│ ╭───╮ │
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
|
||||
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
|
||||
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
|
||||
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
|
||||
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭─────────────────────────────────────────────── Edit - ───────────────────────────────────────────────╮
|
||||
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Artist: Alex │
|
||||
│Overview: some interesting description of the artist │
|
||||
│Disambiguation: American pianist │
|
||||
│Type: Person │
|
||||
│Status: Continuing │
|
||||
│Genres: soundtrack │
|
||||
│Rating: 84% │
|
||||
│Path: /nfs/music/test-artist │
|
||||
│Quality Profile: Lossless │
|
||||
│Metadata Profile: Standard │
|
||||
│Monitored: Yes │
|
||||
│Albums: 1 │
|
||||
│Tracks: 15/15 │
|
||||
│Size on Disk: 0.00 GB │
|
||||
│ │
|
||||
│ │
|
||||
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||
││ Albums ││
|
||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
││ ││
|
||||
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+2
-2
@@ -4,7 +4,7 @@ expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ expression: output
|
||||
|
||||
╭───────────────────── Delete Artist ─────────────────────╮
|
||||
│ Do you really want to delete the artist: │
|
||||
│ ? │
|
||||
│ Alex? │
|
||||
│ │
|
||||
│ │
|
||||
│ ╭───╮ │
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ ╭───╮ │
|
||||
│ Monitored: │ ✔ │ │
|
||||
│ ╰───╯ │
|
||||
│ ╭───────────────────────────────╮──────────────────────────────╮ │
|
||||
│ Monitor│Standard │ ▼ │ │
|
||||
│ │ │──────────────────────────────╯ │
|
||||
│ │ │──────────────────────────────╮ │
|
||||
│ Qual│ │ ▼ │ │
|
||||
│ │ │──────────────────────────────╯ │
|
||||
│ │ │──────────────────────────────╮ │
|
||||
│ Metad│ │ ▼ │ │
|
||||
│ │ │──────────────────────────────╯ │
|
||||
│ │ │──────────────────────────────╮ │
|
||||
│ │ │ │ │
|
||||
│ │ │──────────────────────────────╯ │
|
||||
│ │ │──────────────────────────────╮ │
|
||||
│ │ │ │ │
|
||||
│ ╰───────────────────────────────╯──────────────────────────────╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
|
||||
││ Save ││ Cancel ││
|
||||
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
source: src/ui/lidarr_ui/library/library_ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ ╭───╮ │
|
||||
│ Monitored: │ ✔ │ │
|
||||
│ ╰───╯ │
|
||||
│ ╭─────────────────────────────────────────────────╮ │
|
||||
│ Monitor New Albums: │All Albums ▼ │ │
|
||||
│ ╰─────────────────────────────────────────────────╯ │
|
||||
│ ╭─────────────────────────────────────────────────╮ │
|
||||
│ Quality Profile: │Lossless ▼ │ │
|
||||
│ ╰─────────────────────────────────────────────────╯ │
|
||||
│ ╭─────────────────────────────────────────────────╮ │
|
||||
│ Metadata Profile: │Standard ▼ │ │
|
||||
│ ╰─────────────────────────────────────────────────╯ │
|
||||
│ ╭─────────────────────────────────────────────────╮ │
|
||||
│ Path: │/nfs/music │ │
|
||||
│ ╰─────────────────────────────────────────────────╯ │
|
||||
│ ╭─────────────────────────────────────────────────╮ │
|
||||
│ Tags: │alex │ │
|
||||
│ ╰─────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
|
||||
││ Save ││ Cancel ││
|
||||
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
+1
-1
@@ -4,7 +4,7 @@ expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ expression: output
|
||||
---
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||
=> Continuing 0 0.00 GB
|
||||
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user