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::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
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)]
|
#[cfg(test)]
|
||||||
@@ -36,6 +36,25 @@ pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
|||||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
(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;
|
pub(in crate::app) struct LidarrContextClueProvider;
|
||||||
|
|
||||||
impl ContextClueProvider for LidarrContextClueProvider {
|
impl ContextClueProvider for LidarrContextClueProvider {
|
||||||
@@ -45,6 +64,11 @@ impl ContextClueProvider for LidarrContextClueProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match active_lidarr_block {
|
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 => {
|
ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults => {
|
||||||
Some(&BARE_POPUP_CONTEXT_CLUES)
|
Some(&BARE_POPUP_CONTEXT_CLUES)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::app::context_clues::{
|
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::key_binding::DEFAULT_KEYBINDINGS;
|
||||||
use crate::app::lidarr::lidarr_context_clues::{
|
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 crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
@@ -65,6 +68,50 @@ mod tests {
|
|||||||
assert_none!(artists_context_clues_iter.next());
|
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]
|
#[test]
|
||||||
fn test_add_artist_search_results_context_clues() {
|
fn test_add_artist_search_results_context_clues() {
|
||||||
let mut add_artist_search_results_context_clues_iter =
|
let mut add_artist_search_results_context_clues_iter =
|
||||||
@@ -92,6 +139,23 @@ mod tests {
|
|||||||
LidarrContextClueProvider::get_context_clues(&mut app);
|
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]
|
#[test]
|
||||||
fn test_lidarr_context_clue_provider_artists_block() {
|
fn test_lidarr_context_clue_provider_artists_block() {
|
||||||
let mut app = App::test_default();
|
let mut app = App::test_default();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod tests {
|
|||||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||||
use crate::network::NetworkEvent;
|
use crate::network::NetworkEvent;
|
||||||
use crate::network::lidarr_network::LidarrEvent;
|
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 pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
@@ -32,6 +33,23 @@ mod tests {
|
|||||||
assert_eq!(app.tick_count, 0);
|
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]
|
#[tokio::test]
|
||||||
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
|
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
|
||||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||||
@@ -60,4 +78,12 @@ mod tests {
|
|||||||
|
|
||||||
assert_str_eq!(query, "Test Artist");
|
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())
|
.dispatch_network_event(LidarrEvent::ListArtists.into())
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
ActiveLidarrBlock::ArtistDetails => {
|
||||||
|
self
|
||||||
|
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
ActiveLidarrBlock::AddArtistSearchResults => {
|
ActiveLidarrBlock::AddArtistSearchResults => {
|
||||||
self
|
self
|
||||||
.dispatch_network_event(
|
.dispatch_network_event(
|
||||||
@@ -53,6 +58,10 @@ impl App<'_> {
|
|||||||
.clone()
|
.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) {
|
async fn check_for_lidarr_prompt_action(&mut self) {
|
||||||
if self.data.lidarr_data.prompt_confirm {
|
if self.data.lidarr_data.prompt_confirm {
|
||||||
self.data.lidarr_data.prompt_confirm = false;
|
self.data.lidarr_data.prompt_confirm = false;
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ mod get_command_handler_tests;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
pub enum LidarrGetCommand {
|
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")]
|
#[command(about = "Get detailed information for the artist with the given ID")]
|
||||||
ArtistDetails {
|
ArtistDetails {
|
||||||
#[arg(
|
#[arg(
|
||||||
@@ -62,6 +71,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHan
|
|||||||
|
|
||||||
async fn handle(self) -> Result<String> {
|
async fn handle(self) -> Result<String> {
|
||||||
let result = match self.command {
|
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 } => {
|
LidarrGetCommand::ArtistDetails { artist_id } => {
|
||||||
let resp = self
|
let resp = self
|
||||||
.network
|
.network
|
||||||
|
|||||||
@@ -23,6 +23,32 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_artist_details_requires_artist_id() {
|
fn test_artist_details_requires_artist_id() {
|
||||||
let result =
|
let result =
|
||||||
@@ -91,6 +117,32 @@ mod tests {
|
|||||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
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]
|
#[tokio::test]
|
||||||
async fn test_handle_get_artist_details_command() {
|
async fn test_handle_get_artist_details_command() {
|
||||||
let expected_artist_id = 1;
|
let expected_artist_id = 1;
|
||||||
|
|||||||
@@ -75,6 +75,31 @@ mod tests {
|
|||||||
assert_ok!(&result);
|
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]
|
#[test]
|
||||||
fn test_search_new_artist_requires_query() {
|
fn test_search_new_artist_requires_query() {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "search-new-artist"]);
|
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::add_command_handler::LidarrAddCommand;
|
||||||
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
|
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
|
||||||
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
|
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
|
||||||
|
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{
|
cli::{
|
||||||
@@ -306,5 +332,37 @@ mod tests {
|
|||||||
|
|
||||||
assert_ok!(&result);
|
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 std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Subcommand;
|
use clap::{Subcommand, arg};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -18,6 +18,15 @@ mod list_command_handler_tests;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
pub enum LidarrListCommand {
|
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")]
|
#[command(about = "List all artists in your Lidarr library")]
|
||||||
Artists,
|
Artists,
|
||||||
#[command(about = "List all Lidarr metadata profiles")]
|
#[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> {
|
async fn handle(self) -> Result<String> {
|
||||||
let result = match self.command {
|
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 => {
|
LidarrListCommand::Artists => {
|
||||||
let resp = self
|
let resp = self
|
||||||
.network
|
.network
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ mod tests {
|
|||||||
|
|
||||||
mod cli {
|
mod cli {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use clap::{Parser, error::ErrorKind};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
@@ -29,6 +31,32 @@ mod tests {
|
|||||||
|
|
||||||
assert_ok!(&result);
|
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 {
|
mod handler {
|
||||||
@@ -77,5 +105,27 @@ mod tests {
|
|||||||
|
|
||||||
assert_ok!(&result);
|
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 list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||||
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
|
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use trigger_automatic_search_command_handler::{
|
||||||
|
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::network::lidarr_network::LidarrEvent;
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
use crate::{app::App, network::NetworkTrait};
|
use crate::{app::App, network::NetworkTrait};
|
||||||
@@ -21,6 +24,7 @@ mod edit_command_handler;
|
|||||||
mod get_command_handler;
|
mod get_command_handler;
|
||||||
mod list_command_handler;
|
mod list_command_handler;
|
||||||
mod refresh_command_handler;
|
mod refresh_command_handler;
|
||||||
|
mod trigger_automatic_search_command_handler;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "lidarr_command_tests.rs"]
|
#[path = "lidarr_command_tests.rs"]
|
||||||
@@ -58,6 +62,11 @@ pub enum LidarrCommand {
|
|||||||
about = "Commands to refresh the data in your Lidarr instance"
|
about = "Commands to refresh the data in your Lidarr instance"
|
||||||
)]
|
)]
|
||||||
Refresh(LidarrRefreshCommand),
|
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")]
|
#[command(about = "Search for a new artist to add to Lidarr")]
|
||||||
SearchNewArtist {
|
SearchNewArtist {
|
||||||
#[arg(
|
#[arg(
|
||||||
@@ -67,6 +76,17 @@ pub enum LidarrCommand {
|
|||||||
)]
|
)]
|
||||||
query: String,
|
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(
|
#[command(
|
||||||
about = "Toggle monitoring for the specified artist corresponding to the given artist ID"
|
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()
|
.handle()
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
|
||||||
|
LidarrTriggerAutomaticSearchCommandHandler::with(
|
||||||
|
self.app,
|
||||||
|
trigger_automatic_search_command,
|
||||||
|
self.network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
LidarrCommand::SearchNewArtist { query } => {
|
LidarrCommand::SearchNewArtist { query } => {
|
||||||
let resp = self
|
let resp = self
|
||||||
.network
|
.network
|
||||||
@@ -144,6 +173,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
|
|||||||
.await?;
|
.await?;
|
||||||
serde_json::to_string_pretty(&resp)?
|
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 } => {
|
LidarrCommand::ToggleArtistMonitoring { artist_id } => {
|
||||||
let resp = self
|
let resp = self
|
||||||
.network
|
.network
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ mod refresh_command_handler_tests;
|
|||||||
pub enum LidarrRefreshCommand {
|
pub enum LidarrRefreshCommand {
|
||||||
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
|
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
|
||||||
AllArtists,
|
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 {
|
impl From<LidarrRefreshCommand> for Command {
|
||||||
@@ -57,6 +66,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
|
|||||||
.await?;
|
.await?;
|
||||||
serde_json::to_string_pretty(&resp)?
|
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)
|
Ok(result)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ mod tests {
|
|||||||
|
|
||||||
mod cli {
|
mod cli {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use clap::{Parser, error::ErrorKind};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_refresh_all_artists_has_no_arg_requirements() {
|
fn test_refresh_all_artists_has_no_arg_requirements() {
|
||||||
@@ -28,6 +30,37 @@ mod tests {
|
|||||||
|
|
||||||
assert_ok!(&result);
|
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 {
|
mod handler {
|
||||||
@@ -68,5 +101,29 @@ mod tests {
|
|||||||
|
|
||||||
assert_ok!(&result);
|
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::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options};
|
||||||
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
|
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||||
ADD_ARTIST_BLOCKS, ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS,
|
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ARTIST_BLOCKS,
|
||||||
EDIT_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
|
EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||||
use crate::network::lidarr_network::LidarrEvent;
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
@@ -26,6 +26,7 @@ mod tests {
|
|||||||
fn test_library_handler_accepts() {
|
fn test_library_handler_accepts() {
|
||||||
let mut library_handler_blocks = Vec::new();
|
let mut library_handler_blocks = Vec::new();
|
||||||
library_handler_blocks.extend(LIBRARY_BLOCKS);
|
library_handler_blocks.extend(LIBRARY_BLOCKS);
|
||||||
|
library_handler_blocks.extend(ARTIST_DETAILS_BLOCKS);
|
||||||
library_handler_blocks.extend(DELETE_ARTIST_BLOCKS);
|
library_handler_blocks.extend(DELETE_ARTIST_BLOCKS);
|
||||||
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
|
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
|
||||||
library_handler_blocks.extend(ADD_ARTIST_BLOCKS);
|
library_handler_blocks.extend(ADD_ARTIST_BLOCKS);
|
||||||
@@ -587,6 +588,32 @@ mod tests {
|
|||||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
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]
|
#[test]
|
||||||
fn test_edit_key() {
|
fn test_edit_key() {
|
||||||
let mut app = App::test_default();
|
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};
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
|
|
||||||
mod add_artist_handler;
|
mod add_artist_handler;
|
||||||
|
mod artist_details_handler;
|
||||||
mod delete_artist_handler;
|
mod delete_artist_handler;
|
||||||
mod edit_artist_handler;
|
mod edit_artist_handler;
|
||||||
|
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
pub(in crate::handlers::lidarr_handlers) use add_artist_handler::AddArtistHandler;
|
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 delete_artist_handler::DeleteArtistHandler;
|
||||||
pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler;
|
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)
|
EditArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||||
.handle();
|
.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(),
|
_ => self.handle_key_event(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +89,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
|
|||||||
AddArtistHandler::accepts(active_block)
|
AddArtistHandler::accepts(active_block)
|
||||||
|| DeleteArtistHandler::accepts(active_block)
|
|| DeleteArtistHandler::accepts(active_block)
|
||||||
|| EditArtistHandler::accepts(active_block)
|
|| EditArtistHandler::accepts(active_block)
|
||||||
|
|| ArtistDetailsHandler::accepts(active_block)
|
||||||
|| LIBRARY_BLOCKS.contains(&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) {
|
fn handle_submit(&mut self) {
|
||||||
if self.active_lidarr_block == ActiveLidarrBlock::UpdateAllArtistsPrompt {
|
match self.active_lidarr_block {
|
||||||
if self.app.data.lidarr_data.prompt_confirm {
|
ActiveLidarrBlock::Artists => {
|
||||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
|
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()
|
ActiveSonarrBlock::SeriesDetails.into()
|
||||||
);
|
);
|
||||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
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);
|
assert!(!app.is_routing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,6 +245,14 @@ pub struct AddArtistSearchResult {
|
|||||||
pub ratings: Option<Ratings>,
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub struct DeleteArtistParams {
|
pub struct DeleteArtistParams {
|
||||||
@@ -291,6 +299,44 @@ pub struct EditArtistParams {
|
|||||||
pub clear_tags: bool,
|
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 {
|
impl From<LidarrSerdeable> for Serdeable {
|
||||||
fn from(value: LidarrSerdeable) -> Serdeable {
|
fn from(value: LidarrSerdeable) -> Serdeable {
|
||||||
Serdeable::Lidarr(value)
|
Serdeable::Lidarr(value)
|
||||||
@@ -300,6 +346,8 @@ impl From<LidarrSerdeable> for Serdeable {
|
|||||||
serde_enum_from!(
|
serde_enum_from!(
|
||||||
LidarrSerdeable {
|
LidarrSerdeable {
|
||||||
AddArtistSearchResults(Vec<AddArtistSearchResult>),
|
AddArtistSearchResults(Vec<AddArtistSearchResult>),
|
||||||
|
Albums(Vec<Album>),
|
||||||
|
Album(Album),
|
||||||
Artist(Artist),
|
Artist(Artist),
|
||||||
Artists(Vec<Artist>),
|
Artists(Vec<Artist>),
|
||||||
DiskSpaces(Vec<DiskSpace>),
|
DiskSpaces(Vec<DiskSpace>),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ mod tests {
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::models::lidarr_models::{
|
use crate::models::lidarr_models::{
|
||||||
AddArtistSearchResult, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
|
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
|
||||||
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
|
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
@@ -401,6 +401,45 @@ mod tests {
|
|||||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Tags(tags));
|
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]
|
#[test]
|
||||||
fn test_artist_status_display() {
|
fn test_artist_status_display() {
|
||||||
assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing");
|
assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing");
|
||||||
@@ -501,19 +540,4 @@ mod tests {
|
|||||||
assert!(search_result.genres.is_empty());
|
assert!(search_result.genres.is_empty());
|
||||||
assert_none!(&search_result.ratings);
|
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 serde_json::Number;
|
||||||
|
|
||||||
use super::modals::{AddArtistModal, EditArtistModal};
|
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::{
|
use crate::models::{
|
||||||
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
|
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
|
||||||
lidarr_models::{AddArtistSearchResult, Artist, DownloadRecord},
|
lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord},
|
||||||
servarr_models::{DiskSpace, RootFolder},
|
servarr_models::{DiskSpace, RootFolder},
|
||||||
stateful_table::StatefulTable,
|
stateful_table::StatefulTable,
|
||||||
};
|
};
|
||||||
@@ -19,8 +21,8 @@ use {
|
|||||||
crate::models::stateful_table::SortOption,
|
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::quality_profile_map,
|
||||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||||
add_artist_search_result, download_record, metadata_profile, metadata_profile_map,
|
add_artist_search_result, album, artist, download_record, metadata_profile,
|
||||||
quality_profile, root_folder, tags_map,
|
metadata_profile_map, quality_profile, root_folder, tags_map,
|
||||||
},
|
},
|
||||||
crate::network::servarr_test_utils::diskspace,
|
crate::network::servarr_test_utils::diskspace,
|
||||||
strum::{Display, EnumString, IntoEnumIterator},
|
strum::{Display, EnumString, IntoEnumIterator},
|
||||||
@@ -35,6 +37,8 @@ pub struct LidarrData<'a> {
|
|||||||
pub add_artist_search: Option<HorizontallyScrollableText>,
|
pub add_artist_search: Option<HorizontallyScrollableText>,
|
||||||
pub add_import_list_exclusion: bool,
|
pub add_import_list_exclusion: bool,
|
||||||
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
|
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
|
||||||
|
pub albums: StatefulTable<Album>,
|
||||||
|
pub artist_info_tabs: TabState,
|
||||||
pub artists: StatefulTable<Artist>,
|
pub artists: StatefulTable<Artist>,
|
||||||
pub delete_artist_files: bool,
|
pub delete_artist_files: bool,
|
||||||
pub disk_space_vec: Vec<DiskSpace>,
|
pub disk_space_vec: Vec<DiskSpace>,
|
||||||
@@ -58,6 +62,11 @@ impl LidarrData<'_> {
|
|||||||
self.add_import_list_exclusion = false;
|
self.add_import_list_exclusion = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset_artist_info_tabs(&mut self) {
|
||||||
|
self.albums = StatefulTable::default();
|
||||||
|
self.artist_info_tabs.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String {
|
pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String {
|
||||||
tag_ids
|
tag_ids
|
||||||
.iter()
|
.iter()
|
||||||
@@ -97,6 +106,7 @@ impl<'a> Default for LidarrData<'a> {
|
|||||||
add_artist_search: None,
|
add_artist_search: None,
|
||||||
add_import_list_exclusion: false,
|
add_import_list_exclusion: false,
|
||||||
add_searched_artists: None,
|
add_searched_artists: None,
|
||||||
|
albums: StatefulTable::default(),
|
||||||
artists: StatefulTable::default(),
|
artists: StatefulTable::default(),
|
||||||
delete_artist_files: false,
|
delete_artist_files: false,
|
||||||
disk_space_vec: Vec::new(),
|
disk_space_vec: Vec::new(),
|
||||||
@@ -117,6 +127,12 @@ impl<'a> Default for LidarrData<'a> {
|
|||||||
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
|
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
|
||||||
config: None,
|
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(),
|
tags_map: tags_map(),
|
||||||
..LidarrData::default()
|
..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 {
|
lidarr_data.artists.sorting(vec![SortOption {
|
||||||
name: "Name",
|
name: "Name",
|
||||||
cmp_fn: Some(|a: &Artist, b: &Artist| a.artist_name.text.cmp(&b.artist_name.text)),
|
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 {
|
pub enum ActiveLidarrBlock {
|
||||||
#[default]
|
#[default]
|
||||||
Artists,
|
Artists,
|
||||||
|
ArtistDetails,
|
||||||
ArtistsSortPrompt,
|
ArtistsSortPrompt,
|
||||||
AddArtistAlreadyInLibrary,
|
AddArtistAlreadyInLibrary,
|
||||||
AddArtistConfirmPrompt,
|
AddArtistConfirmPrompt,
|
||||||
@@ -206,6 +225,7 @@ pub enum ActiveLidarrBlock {
|
|||||||
AddArtistSelectQualityProfile,
|
AddArtistSelectQualityProfile,
|
||||||
AddArtistSelectRootFolder,
|
AddArtistSelectRootFolder,
|
||||||
AddArtistTagsInput,
|
AddArtistTagsInput,
|
||||||
|
AutomaticallySearchArtistPrompt,
|
||||||
DeleteArtistPrompt,
|
DeleteArtistPrompt,
|
||||||
DeleteArtistConfirmPrompt,
|
DeleteArtistConfirmPrompt,
|
||||||
DeleteArtistToggleDeleteFile,
|
DeleteArtistToggleDeleteFile,
|
||||||
@@ -220,9 +240,12 @@ pub enum ActiveLidarrBlock {
|
|||||||
EditArtistToggleMonitored,
|
EditArtistToggleMonitored,
|
||||||
FilterArtists,
|
FilterArtists,
|
||||||
FilterArtistsError,
|
FilterArtistsError,
|
||||||
|
SearchAlbums,
|
||||||
|
SearchAlbumsError,
|
||||||
SearchArtists,
|
SearchArtists,
|
||||||
SearchArtistsError,
|
SearchArtistsError,
|
||||||
UpdateAllArtistsPrompt,
|
UpdateAllArtistsPrompt,
|
||||||
|
UpdateAndScanArtistPrompt,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
|
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
|
||||||
@@ -235,6 +258,14 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
|
|||||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
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] = [
|
pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 12] = [
|
||||||
ActiveLidarrBlock::AddArtistAlreadyInLibrary,
|
ActiveLidarrBlock::AddArtistAlreadyInLibrary,
|
||||||
ActiveLidarrBlock::AddArtistConfirmPrompt,
|
ActiveLidarrBlock::AddArtistConfirmPrompt,
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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::{
|
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,
|
DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
@@ -44,6 +47,18 @@ mod tests {
|
|||||||
assert!(!lidarr_data.add_import_list_exclusion);
|
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]
|
#[test]
|
||||||
fn test_tag_ids_to_display() {
|
fn test_tag_ids_to_display() {
|
||||||
let mut tags_map = BiMap::new();
|
let mut tags_map = BiMap::new();
|
||||||
@@ -112,6 +127,7 @@ mod tests {
|
|||||||
assert_none!(lidarr_data.add_artist_search);
|
assert_none!(lidarr_data.add_artist_search);
|
||||||
assert!(!lidarr_data.add_import_list_exclusion);
|
assert!(!lidarr_data.add_import_list_exclusion);
|
||||||
assert_none!(lidarr_data.add_searched_artists);
|
assert_none!(lidarr_data.add_searched_artists);
|
||||||
|
assert_is_empty!(lidarr_data.albums);
|
||||||
assert_is_empty!(lidarr_data.artists);
|
assert_is_empty!(lidarr_data.artists);
|
||||||
assert!(!lidarr_data.delete_artist_files);
|
assert!(!lidarr_data.delete_artist_files);
|
||||||
assert_is_empty!(lidarr_data.disk_space_vec);
|
assert_is_empty!(lidarr_data.disk_space_vec);
|
||||||
@@ -139,6 +155,18 @@ mod tests {
|
|||||||
&ARTISTS_CONTEXT_CLUES
|
&ARTISTS_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_none!(lidarr_data.main_tabs.tabs[0].config);
|
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]
|
#[test]
|
||||||
@@ -153,6 +181,16 @@ mod tests {
|
|||||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::UpdateAllArtistsPrompt));
|
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]
|
#[test]
|
||||||
fn test_add_artist_blocks_contents() {
|
fn test_add_artist_blocks_contents() {
|
||||||
assert_eq!(ADD_ARTIST_BLOCKS.len(), 12);
|
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);
|
app.lock().await.server_tabs.set_index(2);
|
||||||
let mut network = test_network(&app);
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
assert!(
|
assert_ok!(
|
||||||
network
|
network
|
||||||
.handle_lidarr_event(LidarrEvent::ToggleArtistMonitoring(1))
|
.handle_lidarr_event(LidarrEvent::ToggleArtistMonitoring(1))
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
get_mock.assert_async().await;
|
get_mock.assert_async().await;
|
||||||
@@ -167,6 +166,52 @@ mod tests {
|
|||||||
mock.assert_async().await;
|
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]
|
#[tokio::test]
|
||||||
async fn test_handle_edit_artist_event() {
|
async fn test_handle_edit_artist_event() {
|
||||||
let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap();
|
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;
|
mod albums;
|
||||||
use log::{debug, info, warn};
|
mod artists;
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
pub mod test_utils {
|
pub mod test_utils {
|
||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
use crate::models::lidarr_models::{
|
use crate::models::lidarr_models::{
|
||||||
AddArtistSearchResult, Artist, ArtistStatistics, ArtistStatus, DownloadRecord, DownloadStatus,
|
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
|
||||||
DownloadsResponse, EditArtistParams, Member, MetadataProfile, NewItemMonitorType, Ratings,
|
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, Member, MetadataProfile,
|
||||||
SystemStatus,
|
NewItemMonitorType, Ratings, SystemStatus,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{QualityProfile, RootFolder, Tag};
|
use crate::models::servarr_models::{QualityProfile, RootFolder, Tag};
|
||||||
use bimap::BiMap;
|
use bimap::BiMap;
|
||||||
@@ -50,6 +50,27 @@ pub mod test_utils {
|
|||||||
"percentOfTracks": 99.9
|
"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 {
|
pub fn member() -> Member {
|
||||||
Member {
|
Member {
|
||||||
@@ -199,4 +220,33 @@ pub mod test_utils {
|
|||||||
ratings: Some(ratings()),
|
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]
|
#[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");
|
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]
|
#[rstest]
|
||||||
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
|
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
|
||||||
#[case(LidarrEvent::GetDownloads(500), "/queue")]
|
#[case(LidarrEvent::GetDownloads(500), "/queue")]
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ pub enum LidarrEvent {
|
|||||||
DeleteArtist(DeleteArtistParams),
|
DeleteArtist(DeleteArtistParams),
|
||||||
DeleteTag(i64),
|
DeleteTag(i64),
|
||||||
EditArtist(EditArtistParams),
|
EditArtist(EditArtistParams),
|
||||||
|
GetAlbums(i64),
|
||||||
|
GetAlbumDetails(i64),
|
||||||
GetArtistDetails(i64),
|
GetArtistDetails(i64),
|
||||||
GetDiskSpace,
|
GetDiskSpace,
|
||||||
GetDownloads(u64),
|
GetDownloads(u64),
|
||||||
@@ -41,8 +43,11 @@ pub enum LidarrEvent {
|
|||||||
HealthCheck,
|
HealthCheck,
|
||||||
ListArtists,
|
ListArtists,
|
||||||
SearchNewArtist(String),
|
SearchNewArtist(String),
|
||||||
|
ToggleAlbumMonitoring(i64),
|
||||||
ToggleArtistMonitoring(i64),
|
ToggleArtistMonitoring(i64),
|
||||||
|
TriggerAutomaticArtistSearch(i64),
|
||||||
UpdateAllArtists,
|
UpdateAllArtists,
|
||||||
|
UpdateAndScanArtist(i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkResource for LidarrEvent {
|
impl NetworkResource for LidarrEvent {
|
||||||
@@ -55,10 +60,13 @@ impl NetworkResource for LidarrEvent {
|
|||||||
| LidarrEvent::ListArtists
|
| LidarrEvent::ListArtists
|
||||||
| LidarrEvent::AddArtist(_)
|
| LidarrEvent::AddArtist(_)
|
||||||
| LidarrEvent::ToggleArtistMonitoring(_) => "/artist",
|
| LidarrEvent::ToggleArtistMonitoring(_) => "/artist",
|
||||||
|
LidarrEvent::GetAlbums(_) | LidarrEvent::ToggleAlbumMonitoring(_) | LidarrEvent::GetAlbumDetails(_) => "/album",
|
||||||
LidarrEvent::GetDiskSpace => "/diskspace",
|
LidarrEvent::GetDiskSpace => "/diskspace",
|
||||||
LidarrEvent::GetDownloads(_) => "/queue",
|
LidarrEvent::GetDownloads(_) => "/queue",
|
||||||
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
|
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
|
||||||
LidarrEvent::UpdateAllArtists => "/command",
|
LidarrEvent::TriggerAutomaticArtistSearch(_)
|
||||||
|
| LidarrEvent::UpdateAllArtists
|
||||||
|
| LidarrEvent::UpdateAndScanArtist(_) => "/command",
|
||||||
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
|
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
|
||||||
LidarrEvent::GetQualityProfiles => "/qualityprofile",
|
LidarrEvent::GetQualityProfiles => "/qualityprofile",
|
||||||
LidarrEvent::GetRootFolders => "/rootfolder",
|
LidarrEvent::GetRootFolders => "/rootfolder",
|
||||||
@@ -89,10 +97,14 @@ impl Network<'_, '_> {
|
|||||||
.delete_lidarr_tag(tag_id)
|
.delete_lidarr_tag(tag_id)
|
||||||
.await
|
.await
|
||||||
.map(LidarrSerdeable::from),
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetAlbums(artist_id) => {
|
||||||
|
self.get_albums(artist_id).await.map(LidarrSerdeable::from)
|
||||||
|
}
|
||||||
LidarrEvent::GetArtistDetails(artist_id) => self
|
LidarrEvent::GetArtistDetails(artist_id) => self
|
||||||
.get_artist_details(artist_id)
|
.get_artist_details(artist_id)
|
||||||
.await
|
.await
|
||||||
.map(LidarrSerdeable::from),
|
.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::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from),
|
||||||
LidarrEvent::GetDownloads(count) => self
|
LidarrEvent::GetDownloads(count) => self
|
||||||
.get_lidarr_downloads(count)
|
.get_lidarr_downloads(count)
|
||||||
@@ -128,11 +140,23 @@ impl Network<'_, '_> {
|
|||||||
LidarrEvent::SearchNewArtist(query) => {
|
LidarrEvent::SearchNewArtist(query) => {
|
||||||
self.search_artist(query).await.map(LidarrSerdeable::from)
|
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
|
LidarrEvent::ToggleArtistMonitoring(artist_id) => self
|
||||||
.toggle_artist_monitoring(artist_id)
|
.toggle_artist_monitoring(artist_id)
|
||||||
.await
|
.await
|
||||||
.map(LidarrSerdeable::from),
|
.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::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::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from),
|
||||||
LidarrEvent::AddArtist(body) => self.add_artist(body).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::app::App;
|
||||||
use crate::models::Route;
|
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::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||||
use crate::render_selectable_input_box;
|
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::utils::title_block_centered;
|
||||||
use crate::ui::widgets::button::Button;
|
use crate::ui::widgets::button::Button;
|
||||||
use crate::ui::widgets::checkbox::Checkbox;
|
use crate::ui::widgets::checkbox::Checkbox;
|
||||||
@@ -34,7 +37,13 @@ impl DrawUi for EditArtistUi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
|
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| {
|
let draw_edit_artist_prompt = |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| {
|
||||||
draw_edit_artist_confirmation_prompt(f, app, prompt_area);
|
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::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
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::DrawUi;
|
||||||
use crate::ui::lidarr_ui::library::{LibraryUi, decorate_artist_row_with_style};
|
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(DELETE_ARTIST_BLOCKS);
|
||||||
library_ui_blocks.extend(EDIT_ARTIST_BLOCKS);
|
library_ui_blocks.extend(EDIT_ARTIST_BLOCKS);
|
||||||
library_ui_blocks.extend(ADD_ARTIST_BLOCKS);
|
library_ui_blocks.extend(ADD_ARTIST_BLOCKS);
|
||||||
|
library_ui_blocks.extend(ARTIST_DETAILS_BLOCKS);
|
||||||
|
|
||||||
for active_lidarr_block in ActiveLidarrBlock::iter() {
|
for active_lidarr_block in ActiveLidarrBlock::iter() {
|
||||||
if library_ui_blocks.contains(&active_lidarr_block) {
|
if library_ui_blocks.contains(&active_lidarr_block) {
|
||||||
@@ -182,7 +184,7 @@ mod tests {
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::BlockSelectionState;
|
use crate::models::BlockSelectionState;
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
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;
|
use rstest::rstest;
|
||||||
|
|
||||||
@@ -263,5 +265,49 @@ mod tests {
|
|||||||
|
|
||||||
insta::assert_snapshot!(output);
|
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 add_artist_ui::AddArtistUi;
|
||||||
|
use artist_details_ui::ArtistDetailsUi;
|
||||||
use delete_artist_ui::DeleteArtistUi;
|
use delete_artist_ui::DeleteArtistUi;
|
||||||
use edit_artist_ui::EditArtistUi;
|
use edit_artist_ui::EditArtistUi;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@@ -28,6 +29,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod add_artist_ui;
|
mod add_artist_ui;
|
||||||
|
mod artist_details_ui;
|
||||||
mod delete_artist_ui;
|
mod delete_artist_ui;
|
||||||
mod edit_artist_ui;
|
mod edit_artist_ui;
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ impl DrawUi for LibraryUi {
|
|||||||
return AddArtistUi::accepts(route)
|
return AddArtistUi::accepts(route)
|
||||||
|| DeleteArtistUi::accepts(route)
|
|| DeleteArtistUi::accepts(route)
|
||||||
|| EditArtistUi::accepts(route)
|
|| EditArtistUi::accepts(route)
|
||||||
|
|| ArtistDetailsUi::accepts(route)
|
||||||
|| LIBRARY_BLOCKS.contains(&active_lidarr_block);
|
|| LIBRARY_BLOCKS.contains(&active_lidarr_block);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +60,7 @@ impl DrawUi for LibraryUi {
|
|||||||
_ if AddArtistUi::accepts(route) => AddArtistUi::draw(f, app, area),
|
_ if AddArtistUi::accepts(route) => AddArtistUi::draw(f, app, area),
|
||||||
_ if DeleteArtistUi::accepts(route) => DeleteArtistUi::draw(f, app, area),
|
_ if DeleteArtistUi::accepts(route) => DeleteArtistUi::draw(f, app, area),
|
||||||
_ if EditArtistUi::accepts(route) => EditArtistUi::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, _) => {
|
Route::Lidarr(ActiveLidarrBlock::UpdateAllArtistsPrompt, _) => {
|
||||||
let confirmation_prompt = ConfirmationPrompt::new()
|
let confirmation_prompt = ConfirmationPrompt::new()
|
||||||
.title("Update All Artists")
|
.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 │
|
│ ✔ 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 │
|
│ ✔ 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 ─────────────────────╮
|
╭───────────────────── Delete Artist ─────────────────────╮
|
||||||
│ Do you really want to delete the 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
|
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 ─────────────────────╮
|
╭───────────────────── Delete Artist ─────────────────────╮
|
||||||
│ Do you really want to delete the 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
|
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
|
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
|
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
|
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
|
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
|
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
|
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