Compare commits

...

4 Commits

62 changed files with 1925 additions and 312 deletions
+17 -1
View File
@@ -65,6 +65,7 @@ pub static ARTIST_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
@@ -72,10 +73,25 @@ pub static ARTIST_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_ARTIST_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub(in crate::app) struct LidarrContextClueProvider;
impl ContextClueProvider for LidarrContextClueProvider {
+48 -4
View File
@@ -9,6 +9,7 @@ mod tests {
use crate::app::lidarr::lidarr_context_clues::{
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES,
ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS,
@@ -164,6 +165,10 @@ mod tests {
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
@@ -183,10 +188,6 @@ mod tests {
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
@@ -194,8 +195,51 @@ mod tests {
assert_none!(artist_history_context_clues_iter.next());
}
#[test]
fn test_manual_artist_search_context_clues() {
let mut manual_artist_search_context_clues_iter = MANUAL_ARTIST_SEARCH_CONTEXT_CLUES.iter();
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(manual_artist_search_context_clues_iter.next());
}
#[rstest]
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::ArtistHistory, &ARTIST_HISTORY_CONTEXT_CLUES)]
#[case(2, ActiveLidarrBlock::ManualArtistSearch, &MANUAL_ARTIST_SEARCH_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_artist_info_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
+43 -1
View File
@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::models::lidarr_models::Artist;
use crate::models::lidarr_models::{Artist, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_models::Indexer;
use crate::network::NetworkEvent;
@@ -78,6 +78,48 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_artist_search_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDiscographyReleases(1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_artist_search_block_discography_releases_non_empty() {
let mut app = App::test_default();
app
.data
.lidarr_data
.discography_releases
.set_items(vec![LidarrRelease::default()]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_downloads_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
+9
View File
@@ -45,6 +45,15 @@ impl App<'_> {
)
.await;
}
ActiveLidarrBlock::ManualArtistSearch => {
if self.data.lidarr_data.discography_releases.is_empty() {
self
.dispatch_network_event(
LidarrEvent::GetDiscographyReleases(self.extract_artist_id().await).into(),
)
.await;
}
}
ActiveLidarrBlock::AddArtistSearchResults => {
self
.dispatch_network_event(
+113 -4
View File
@@ -60,6 +60,55 @@ mod tests {
assert_err!(&result);
}
#[test]
fn test_download_release_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--indexer-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--guid",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--guid",
"1",
"--indexer-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_toggle_artist_monitoring_requires_artist_id() {
let result =
@@ -136,7 +185,7 @@ mod tests {
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "start-task"]);
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "start-task"]);
assert_err!(&result);
assert_eq!(
@@ -232,9 +281,10 @@ mod tests {
use crate::cli::lidarr::add_command_handler::LidarrAddCommand;
use crate::cli::lidarr::edit_command_handler::LidarrEditCommand;
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
use crate::models::lidarr_models::LidarrTaskName;
use crate::models::lidarr_models::{LidarrReleaseDownloadBody, LidarrTaskName};
use crate::models::servarr_models::IndexerSettings;
use crate::{
app::App,
@@ -427,15 +477,43 @@ mod tests {
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_series_command = LidarrCommand::Refresh(LidarrRefreshCommand::AllArtists);
let refresh_artist_command = LidarrCommand::Refresh(LidarrRefreshCommand::AllArtists);
let result = LidarrCliHandler::with(&app_arc, refresh_series_command, &mut mock_network)
let result = LidarrCliHandler::with(&app_arc, refresh_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler()
{
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command =
LidarrCommand::ManualSearch(LidarrManualSearchCommand::Discography { artist_id: 1 });
let result =
LidarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{
@@ -468,6 +546,37 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_download_release_command() {
let expected_release_download_body = LidarrReleaseDownloadBody {
guid: "guid".to_owned(),
indexer_id: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DownloadRelease(expected_release_download_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_release_command = LidarrCommand::DownloadRelease {
guid: "guid".to_owned(),
indexer_id: 1,
};
let result = LidarrCliHandler::with(&app_arc, download_release_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_toggle_artist_monitoring_command() {
let mut mock_network = MockNetworkTrait::new();
@@ -0,0 +1,84 @@
use crate::app::App;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::{CliCommandHandler, Command};
use crate::models::Serdeable;
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
use crate::network::NetworkTrait;
use crate::network::lidarr_network::LidarrEvent;
use anyhow::Result;
use clap::Subcommand;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::Mutex;
#[cfg(test)]
#[path = "manual_search_command_handler_tests.rs"]
mod manual_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrManualSearchCommand {
#[command(
about = "Trigger a manual search of discography releases for the given artist corresponding to the artist with the given ID."
)]
Discography {
#[arg(
long,
help = "The Lidarr ID of the artist whose discography releases you wish to fetch and list",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrManualSearchCommand> for Command {
fn from(value: LidarrManualSearchCommand) -> Self {
Command::Lidarr(LidarrCommand::ManualSearch(value))
}
}
pub(super) struct LidarrManualSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrManualSearchCommand>
for LidarrManualSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrManualSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrManualSearchCommand::Discography { artist_id } => {
println!("Searching for artist discography releases. This may take a minute...");
match self
.network
.handle_network_event(LidarrEvent::GetDiscographyReleases(artist_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => {
let discography_vec: Vec<LidarrRelease> = releases_vec
.into_iter()
.filter(|release| release.discography)
.collect();
serde_json::to_string_pretty(&discography_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
};
Ok(result)
}
}
@@ -0,0 +1,98 @@
#[cfg(test)]
mod tests {
use crate::cli::Command;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_manual_search_command_from() {
let command = LidarrManualSearchCommand::Discography { artist_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Lidarr(LidarrCommand::ManualSearch(command))
);
}
mod cli {
use crate::Cli;
use clap::CommandFactory;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_manual_discography_search_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "manual-search", "discography"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_discography_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"discography",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use crate::app::App;
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::manual_search_command_handler::{
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{MockNetworkTrait, NetworkEvent};
use mockall::predicate::eq;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_manual_discography_search_command() {
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_discography_search_command =
LidarrManualSearchCommand::Discography { artist_id: 1 };
let result = LidarrManualSearchCommandHandler::with(
&app_arc,
manual_discography_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
+31 -1
View File
@@ -15,7 +15,10 @@ use trigger_automatic_search_command_handler::{
};
use super::{CliCommandHandler, Command};
use crate::models::lidarr_models::LidarrTaskName;
use crate::cli::lidarr::manual_search_command_handler::{
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::lidarr_models::{LidarrReleaseDownloadBody, LidarrTaskName};
use crate::network::lidarr_network::LidarrEvent;
use crate::{app::App, network::NetworkTrait};
@@ -24,6 +27,7 @@ mod delete_command_handler;
mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod manual_search_command_handler;
mod refresh_command_handler;
mod trigger_automatic_search_command_handler;
@@ -63,11 +67,24 @@ pub enum LidarrCommand {
about = "Commands to refresh the data in your Lidarr instance"
)]
Refresh(LidarrRefreshCommand),
#[command(subcommand, about = "Commands to manually search for releases")]
ManualSearch(LidarrManualSearchCommand),
#[command(
subcommand,
about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance"
)]
TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand),
#[command(about = "Manually download the given release")]
DownloadRelease {
#[arg(long, help = "The GUID of the release to download", required = true)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
},
#[command(about = "Mark the Lidarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
@@ -186,6 +203,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
.handle()
.await?
}
LidarrCommand::ManualSearch(manual_search_command) => {
LidarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network)
.handle()
.await?
}
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
LidarrTriggerAutomaticSearchCommandHandler::with(
self.app,
@@ -195,6 +217,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
.handle()
.await?
}
LidarrCommand::DownloadRelease { guid, indexer_id } => {
let params = LidarrReleaseDownloadBody { guid, indexer_id };
let resp = self
.network
.handle_network_event(LidarrEvent::DownloadRelease(params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
@@ -2,16 +2,18 @@ use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use serde_json::json;
use tokio::sync::Mutex;
use super::SonarrCommand;
use crate::models::Serdeable;
use crate::models::sonarr_models::{SonarrRelease, SonarrSerdeable};
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "manual_search_command_handler_tests.rs"]
mod manual_search_command_handler_tests;
@@ -28,7 +30,7 @@ pub enum SonarrManualSearchCommand {
episode_id: i64,
},
#[command(
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID.\nNote that when downloading a season release, ensure that the release includes 'fullSeason: true', otherwise you'll run into issues"
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID"
)]
Season {
#[arg(
@@ -84,11 +86,21 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
season_number,
} => {
println!("Searching for season releases. This may take a minute...");
let resp = self
match self
.network
.handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into())
.await?;
serde_json::to_string_pretty(&resp)?
.await
{
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => {
let seasons_vec: Vec<SonarrRelease> = releases_vec
.into_iter()
.filter(|release| release.full_season)
.collect();
serde_json::to_string_pretty(&seasons_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
};
@@ -5,13 +5,17 @@ use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbum
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, LidarrHistoryItem};
use crate::models::lidarr_models::{
Album, LidarrHistoryItem, LidarrRelease, LidarrReleaseDownloadBody,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS,
EDIT_ARTIST_SELECTION_BLOCKS,
};
use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, Route};
use crate::network::lidarr_network::LidarrEvent;
use serde_json::Number;
#[cfg(test)]
#[path = "artist_details_handler_tests.rs"]
@@ -53,21 +57,23 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
.filter_error_block(ActiveLidarrBlock::FilterArtistHistoryError.into())
.filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text);
let artist_releases_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::ManualArtistSearch.into())
.sorting_block(ActiveLidarrBlock::ManualArtistSearchSortPrompt.into())
.sort_options(releases_sorting_options());
if !handle_table(
self,
|app| &mut app.data.lidarr_data.albums,
albums_table_handling_config,
) && !handle_table(
self,
|app| {
app
.data
.lidarr_data
.artist_history
.as_mut()
.expect("Artist history is undefined")
},
|app| &mut app.data.lidarr_data.artist_history,
artist_history_table_handling_config,
) && !handle_table(
self,
|app| &mut app.data.lidarr_data.discography_releases,
artist_releases_table_handling_config,
) {
match self.active_lidarr_block {
_ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => {
@@ -106,10 +112,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
}
fn is_ready(&self) -> bool {
if self.active_lidarr_block == ActiveLidarrBlock::ArtistHistory {
!self.app.is_loading && self.app.data.lidarr_data.artist_history.is_some()
} else {
!self.app.is_loading
if self.app.is_loading {
return false;
}
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistHistory => !self.app.data.lidarr_data.artist_history.is_empty(),
ActiveLidarrBlock::ManualArtistSearch => {
!self.app.data.lidarr_data.discography_releases.is_empty()
}
_ => true,
}
}
@@ -133,7 +145,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistDetails | ActiveLidarrBlock::ArtistHistory => match self.key {
ActiveLidarrBlock::ArtistDetails
| ActiveLidarrBlock::ArtistHistory
| ActiveLidarrBlock::ManualArtistSearch => match self.key {
_ if matches_key!(left, self.key) => {
self.app.data.lidarr_data.artist_info_tabs.previous();
self.app.pop_and_push_navigation_stack(
@@ -159,7 +173,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
_ => (),
},
ActiveLidarrBlock::UpdateAndScanArtistPrompt
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt
| ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
handle_prompt_toggle(self.app, self.key);
}
_ => (),
@@ -168,20 +183,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistHistory
if !self
.app
.data
.lidarr_data
.artist_history
.as_ref()
.expect("Artist history should be Some")
.is_empty() =>
{
ActiveLidarrBlock::ArtistHistory if !self.app.data.lidarr_data.artist_history.is_empty() => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::ArtistHistoryDetails.into());
}
ActiveLidarrBlock::ManualArtistSearch => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into());
}
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
let LidarrRelease {
guid, indexer_id, ..
} = self
.app
.data
.lidarr_data
.discography_releases
.current_selection()
.clone();
let params = LidarrReleaseDownloadBody { guid, indexer_id };
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DownloadRelease(params));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(
@@ -206,7 +235,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::UpdateAndScanArtistPrompt
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt
| ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
@@ -219,25 +249,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
.data
.lidarr_data
.artist_history
.as_ref()
.expect("Artist history is not populated")
.filtered_items
.is_some()
{
self
.app
.data
.lidarr_data
.artist_history
.as_mut()
.expect("Artist history is not populated")
.reset_filter();
self.app.data.lidarr_data.artist_history.reset_filter();
} else {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_artist_info_tabs();
}
}
ActiveLidarrBlock::ArtistDetails => {
ActiveLidarrBlock::ArtistDetails | ActiveLidarrBlock::ManualArtistSearch => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_artist_info_tabs();
}
@@ -287,7 +308,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
}
_ => (),
},
ActiveLidarrBlock::ArtistHistory => match self.key {
ActiveLidarrBlock::ArtistHistory | ActiveLidarrBlock::ManualArtistSearch => match self.key {
_ if matches_key!(refresh, key) => self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into()),
@@ -334,6 +355,25 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
self.app.pop_navigation_stack();
}
}
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
let LidarrRelease {
guid, indexer_id, ..
} = self
.app
.data
.lidarr_data
.discography_releases
.current_selection()
.clone();
let params = LidarrReleaseDownloadBody { guid, indexer_id };
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DownloadRelease(params));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -346,3 +386,61 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
self.app.get_current_route()
}
}
fn releases_sorting_options() -> Vec<SortOption<LidarrRelease>> {
vec![
SortOption {
name: "Source",
cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)),
},
SortOption {
name: "Age",
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
},
SortOption {
name: "Rejected",
cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)),
},
SortOption {
name: "Title",
cmp_fn: Some(|a, b| {
a.title
.text
.to_lowercase()
.cmp(&b.title.text.to_lowercase())
}),
},
SortOption {
name: "Indexer",
cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())),
},
SortOption {
name: "Size",
cmp_fn: Some(|a, b| a.size.cmp(&b.size)),
},
SortOption {
name: "Peers",
cmp_fn: Some(|a, b| {
let default_number = Number::from(i64::MAX);
let seeder_a = a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
seeder_a.cmp(&seeder_b)
}),
},
SortOption {
name: "Quality",
cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)),
},
]
}
@@ -8,10 +8,10 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
mod test_handle_delete {
use super::*;
@@ -86,7 +86,6 @@ mod tests {
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::StatefulTable;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
use crate::{assert_navigation_popped, assert_navigation_pushed};
@@ -145,9 +144,11 @@ mod tests {
#[test]
fn test_artist_history_submit() {
let mut app = App::test_default();
let mut artist_history = StatefulTable::default();
artist_history.set_items(vec![LidarrHistoryItem::default()]);
app.data.lidarr_data.artist_history = Some(artist_history);
app
.data
.lidarr_data
.artist_history
.set_items(vec![LidarrHistoryItem::default()]);
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None)
.handle();
@@ -159,7 +160,6 @@ mod tests {
fn test_artist_history_submit_no_op_when_artist_history_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into());
app.data.lidarr_data.artist_history = Some(StatefulTable::default());
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None)
.handle();
@@ -218,13 +218,12 @@ mod tests {
#[test]
fn test_artist_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() {
let mut app = App::test_default();
let artist_history = StatefulTable {
app.data.lidarr_data.artist_history = StatefulTable {
filter: Some("Test".into()),
filtered_items: Some(vec![LidarrHistoryItem::default()]),
filtered_state: Some(TableState::default()),
..StatefulTable::default()
};
app.data.lidarr_data.artist_history = Some(artist_history);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into());
@@ -234,25 +233,9 @@ mod tests {
app.get_current_route(),
ActiveLidarrBlock::ArtistHistory.into()
);
assert_none!(app.data.lidarr_data.artist_history.as_ref().unwrap().filter);
assert_none!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.filtered_items
);
assert_none!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.filtered_state
);
assert_none!(app.data.lidarr_data.artist_history.filter);
assert_none!(app.data.lidarr_data.artist_history.filtered_items);
assert_none!(app.data.lidarr_data.artist_history.filtered_state);
}
#[rstest]
@@ -691,10 +674,14 @@ mod tests {
}
#[test]
fn test_artist_details_handler_ready_when_not_loading_and_artist_history_is_some() {
fn test_artist_details_handler_ready_when_not_loading_and_artist_history_is_non_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.data.lidarr_data.artist_history = Some(StatefulTable::default());
app
.data
.lidarr_data
.artist_history
.set_items(vec![LidarrHistoryItem::default()]);
let handler = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
+32
View File
@@ -464,6 +464,37 @@ impl Display for LidarrTaskName {
}
}
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct LidarrRelease {
pub guid: String,
pub protocol: String,
#[serde(deserialize_with = "super::from_i64")]
pub age: i64,
pub title: HorizontallyScrollableText,
pub discography: bool,
pub artist_name: Option<String>,
pub album_title: Option<String>,
pub indexer: String,
#[serde(deserialize_with = "super::from_i64")]
pub indexer_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub size: i64,
pub rejected: bool,
pub rejections: Option<Vec<String>>,
pub seeders: Option<Number>,
pub leechers: Option<Number>,
pub quality: QualityWrapper,
}
#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LidarrReleaseDownloadBody {
pub guid: String,
pub indexer_id: i64,
}
impl From<LidarrSerdeable> for Serdeable {
fn from(value: LidarrSerdeable) -> Serdeable {
Serdeable::Lidarr(value)
@@ -489,6 +520,7 @@ serde_enum_from!(
MetadataProfiles(Vec<MetadataProfile>),
QualityProfiles(Vec<QualityProfile>),
QueueEvents(Vec<QueueEvent>),
Releases(Vec<LidarrRelease>),
RootFolders(Vec<RootFolder>),
SecurityConfig(SecurityConfig),
SystemStatus(SystemStatus),
+14 -2
View File
@@ -6,8 +6,8 @@ mod tests {
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, Member,
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrRelease, LidarrTask,
Member, MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
};
use crate::models::servarr_models::{
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
@@ -460,6 +460,18 @@ mod tests {
assert_eq!(lidarr_serdeable, LidarrSerdeable::RootFolders(root_folders));
}
#[test]
fn test_lidarr_serdeable_from_releases() {
let releases = vec![LidarrRelease {
guid: "test".to_owned(),
..LidarrRelease::default()
}];
let lidarr_serdeable: LidarrSerdeable = releases.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Releases(releases));
}
#[test]
fn test_lidarr_serdeable_from_security_config() {
let security_config = SecurityConfig {
+36 -12
View File
@@ -7,8 +7,9 @@ use crate::app::context_clues::{
};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
};
use crate::models::lidarr_models::LidarrTask;
use crate::models::lidarr_models::{LidarrRelease, LidarrTask};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::{IndexerSettings, QueueEvent};
use crate::models::stateful_list::StatefulList;
@@ -35,6 +36,9 @@ use {
metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map,
},
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{log_line, task},
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
torrent_release, usenet_release,
},
crate::network::servarr_test_utils::diskspace,
crate::network::servarr_test_utils::indexer_test_result,
crate::network::servarr_test_utils::queued_event,
@@ -54,10 +58,11 @@ pub struct LidarrData<'a> {
pub add_root_folder_modal: Option<AddRootFolderModal>,
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
pub albums: StatefulTable<Album>,
pub artist_history: Option<StatefulTable<LidarrHistoryItem>>,
pub artist_history: StatefulTable<LidarrHistoryItem>,
pub artist_info_tabs: TabState,
pub artists: StatefulTable<Artist>,
pub delete_files: bool,
pub discography_releases: StatefulTable<LidarrRelease>,
pub disk_space_vec: Vec<DiskSpace>,
pub downloads: StatefulTable<DownloadRecord>,
pub edit_artist_modal: Option<EditArtistModal>,
@@ -92,7 +97,8 @@ impl LidarrData<'_> {
pub fn reset_artist_info_tabs(&mut self) {
self.albums = StatefulTable::default();
self.artist_history = None;
self.discography_releases = StatefulTable::default();
self.artist_history = StatefulTable::default();
self.artist_info_tabs.index = 0;
}
@@ -137,9 +143,10 @@ impl<'a> Default for LidarrData<'a> {
add_root_folder_modal: None,
add_searched_artists: None,
albums: StatefulTable::default(),
artist_history: None,
artist_history: StatefulTable::default(),
artists: StatefulTable::default(),
delete_files: false,
discography_releases: StatefulTable::default(),
disk_space_vec: Vec::new(),
downloads: StatefulTable::default(),
edit_artist_modal: None,
@@ -214,6 +221,12 @@ impl<'a> Default for LidarrData<'a> {
contextual_help: Some(&ARTIST_HISTORY_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "Manual Search".to_string(),
route: ActiveLidarrBlock::ManualArtistSearch.into(),
contextual_help: Some(&MANUAL_ARTIST_SEARCH_CONTEXT_CLUES),
config: None,
},
]),
}
}
@@ -291,14 +304,7 @@ impl LidarrData<'_> {
let mut indexer_test_all_results = StatefulTable::default();
indexer_test_all_results.set_items(vec![indexer_test_result()]);
let mut artist_history = StatefulTable::default();
artist_history.set_items(vec![lidarr_history_item()]);
artist_history.sorting(vec![sort_option!(id)]);
artist_history.search = Some("artist history search".into());
artist_history.filter = Some("artist history filter".into());
let mut lidarr_data = LidarrData {
artist_history: Some(artist_history),
delete_files: true,
disk_space_vec: vec![diskspace()],
quality_profile_map: quality_profile_map(),
@@ -316,6 +322,12 @@ impl LidarrData<'_> {
version: "1.2.3.4".to_owned(),
..LidarrData::default()
};
lidarr_data
.artist_history
.set_items(vec![lidarr_history_item()]);
lidarr_data.artist_history.sorting(vec![sort_option!(id)]);
lidarr_data.artist_history.search = Some("artist history search".into());
lidarr_data.artist_history.filter = Some("artist history filter".into());
lidarr_data.albums.set_items(vec![album()]);
lidarr_data.albums.search = Some("album search".into());
lidarr_data.artists.set_items(vec![artist()]);
@@ -333,6 +345,12 @@ impl LidarrData<'_> {
}]);
lidarr_data.history.search = Some("test search".into());
lidarr_data.history.filter = Some("test filter".into());
lidarr_data
.discography_releases
.set_items(vec![torrent_release(), usenet_release()]);
lidarr_data
.discography_releases
.sorting(vec![sort_option!(indexer_id)]);
lidarr_data.root_folders.set_items(vec![root_folder()]);
lidarr_data.indexers.set_items(vec![indexer()]);
lidarr_data.queued_events.set_items(vec![queued_event()]);
@@ -358,6 +376,9 @@ pub enum ActiveLidarrBlock {
ArtistHistoryDetails,
ArtistHistorySortPrompt,
ArtistsSortPrompt,
ManualArtistSearch,
ManualArtistSearchConfirmPrompt,
ManualArtistSearchSortPrompt,
AddArtistAlreadyInLibrary,
AddArtistConfirmPrompt,
AddArtistEmptySearchResults,
@@ -459,7 +480,7 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::UpdateAllArtistsPrompt,
];
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 12] = [
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 15] = [
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ArtistHistoryDetails,
@@ -467,6 +488,9 @@ pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 12] = [
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::FilterArtistHistory,
ActiveLidarrBlock::FilterArtistHistoryError,
ActiveLidarrBlock::ManualArtistSearch,
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
ActiveLidarrBlock::ManualArtistSearchSortPrompt,
ActiveLidarrBlock::SearchAlbums,
ActiveLidarrBlock::SearchAlbumsError,
ActiveLidarrBlock::SearchArtistHistory,
@@ -6,8 +6,9 @@ mod tests {
};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
};
use crate::models::lidarr_models::Album;
use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ARTIST_DETAILS_BLOCKS,
DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS,
@@ -16,7 +17,6 @@ mod tests {
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
use crate::models::{
BlockSelectionState, Route,
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS, LidarrData},
@@ -60,13 +60,19 @@ mod tests {
fn test_reset_artist_info_tabs() {
let mut lidarr_data = LidarrData::default();
lidarr_data.albums.set_items(vec![Album::default()]);
lidarr_data.artist_history = Some(StatefulTable::default());
lidarr_data
.discography_releases
.set_items(vec![LidarrRelease::default()]);
lidarr_data
.artist_history
.set_items(vec![LidarrHistoryItem::default()]);
lidarr_data.artist_info_tabs.index = 1;
lidarr_data.reset_artist_info_tabs();
assert_is_empty!(lidarr_data.albums);
assert_none!(lidarr_data.artist_history);
assert_is_empty!(lidarr_data.discography_releases);
assert_is_empty!(lidarr_data.artist_history);
assert_eq!(lidarr_data.artist_info_tabs.index, 0);
}
@@ -140,12 +146,13 @@ mod tests {
assert_none!(lidarr_data.add_searched_artists);
assert_is_empty!(lidarr_data.albums);
assert_is_empty!(lidarr_data.artists);
assert_none!(lidarr_data.artist_history);
assert_is_empty!(lidarr_data.artist_history);
assert!(!lidarr_data.delete_files);
assert_is_empty!(lidarr_data.disk_space_vec);
assert_is_empty!(lidarr_data.downloads);
assert_none!(lidarr_data.edit_artist_modal);
assert_none!(lidarr_data.add_root_folder_modal);
assert_is_empty!(lidarr_data.discography_releases);
assert_is_empty!(lidarr_data.history);
assert_is_empty!(lidarr_data.logs);
assert_is_empty!(lidarr_data.log_details);
@@ -230,7 +237,7 @@ mod tests {
);
assert_none!(lidarr_data.main_tabs.tabs[5].config);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 2);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 3);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
assert_eq!(
lidarr_data.artist_info_tabs.tabs[0].route,
@@ -252,6 +259,17 @@ mod tests {
&ARTIST_HISTORY_CONTEXT_CLUES
);
assert_none!(lidarr_data.artist_info_tabs.tabs[1].config);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[2].title, "Manual Search");
assert_eq!(
lidarr_data.artist_info_tabs.tabs[2].route,
ActiveLidarrBlock::ManualArtistSearch.into()
);
assert_some_eq_x!(
&lidarr_data.artist_info_tabs.tabs[2].contextual_help,
&MANUAL_ARTIST_SEARCH_CONTEXT_CLUES
);
assert_none!(lidarr_data.artist_info_tabs.tabs[2].config);
}
#[test]
@@ -268,7 +286,7 @@ mod tests {
#[test]
fn test_artist_details_blocks_contains_expected_blocks() {
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 12);
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 15);
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistDetails));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistory));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistoryDetails));
@@ -276,6 +294,9 @@ mod tests {
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchArtistPrompt));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistHistory));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistHistoryError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualArtistSearch));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualArtistSearchConfirmPrompt));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualArtistSearchSortPrompt));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbums));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumsError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistHistory));
+1
View File
@@ -537,6 +537,7 @@ pub struct SonarrRelease {
pub quality: QualityWrapper,
pub full_season: bool,
}
#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SonarrReleaseDownloadBody {
@@ -2,14 +2,14 @@
mod tests {
use crate::models::lidarr_models::{
AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams,
LidarrHistoryItem, LidarrSerdeable, MonitorType, NewItemMonitorType,
LidarrHistoryItem, LidarrRelease, LidarrSerdeable, MonitorType, NewItemMonitorType,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::{SortOption, StatefulTable};
use crate::models::stateful_table::SortOption;
use crate::network::NetworkResource;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON, artist, lidarr_history_item,
ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON, artist, lidarr_history_item, torrent_release,
};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use bimap::BiMap;
@@ -156,10 +156,6 @@ mod tests {
.query("artistId=1")
.build_for(LidarrEvent::GetArtistHistory(1))
.await;
let mut artist_history_table = StatefulTable {
sort_asc: true,
..StatefulTable::default()
};
if use_custom_sorting {
let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| {
a.source_title
@@ -173,7 +169,13 @@ mod tests {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
artist_history_table.sorting(vec![history_sort_option]);
app
.lock()
.await
.data
.lidarr_data
.artist_history
.sorting(vec![history_sort_option]);
}
app
.lock()
@@ -182,7 +184,7 @@ mod tests {
.lidarr_data
.artists
.set_items(vec![artist()]);
app.lock().await.data.lidarr_data.artist_history = Some(artist_history_table);
app.lock().await.data.lidarr_data.artist_history.sort_asc = true;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
@@ -195,20 +197,11 @@ mod tests {
};
async_server.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.artist_history);
assert_eq!(
app.data.lidarr_data.artist_history.as_ref().unwrap().items,
app.data.lidarr_data.artist_history.items,
expected_history_items
);
assert!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.sort_asc
);
assert!(app.data.lidarr_data.artist_history.sort_asc);
assert_eq!(history_items, response);
}
@@ -281,20 +274,11 @@ mod tests {
};
async_server.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.artist_history);
assert_eq!(
app.data.lidarr_data.artist_history.as_ref().unwrap().items,
app.data.lidarr_data.artist_history.items,
expected_history_items
);
assert!(
!app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.sort_asc
);
assert!(!app.data.lidarr_data.artist_history.sort_asc);
assert_eq!(history_items, response);
}
@@ -342,12 +326,14 @@ mod tests {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
let mut artist_history_table = StatefulTable {
sort_asc: true,
..StatefulTable::default()
};
artist_history_table.sorting(vec![history_sort_option]);
app.lock().await.data.lidarr_data.artist_history = Some(artist_history_table);
app
.lock()
.await
.data
.lidarr_data
.artist_history
.sorting(vec![history_sort_option]);
app.lock().await.data.lidarr_data.artist_history.sort_asc = true;
app
.lock()
.await
@@ -371,28 +357,92 @@ mod tests {
};
async_server.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.artist_history);
assert!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.is_empty()
);
assert!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.sort_asc
);
assert!(app.data.lidarr_data.artist_history.is_empty());
assert!(app.data.lidarr_data.artist_history.sort_asc);
assert_eq!(history_items, response);
}
#[tokio::test]
async fn test_handle_get_artist_discography_releases_event() {
let release_json = json!([
{
"guid": "1234",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"artistName": "Alex",
"albumTitle": "Something",
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"quality": { "quality": { "name": "Lossless" }},
"discography": true
},
{
"guid": "4567",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"artistName": "Alex",
"albumTitle": "Something",
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"quality": { "quality": { "name": "Lossless" }},
}
]);
let expected_filtered_lidarr_release = LidarrRelease {
discography: true,
..torrent_release()
};
let expected_raw_lidarr_releases = vec![
LidarrRelease {
discography: true,
..torrent_release()
},
LidarrRelease {
guid: "4567".to_owned(),
..torrent_release()
},
];
let (mock, app, _server) = MockServarrApi::get()
.returns(release_json)
.query("artistId=1")
.build_for(LidarrEvent::GetDiscographyReleases(1))
.await;
app
.lock()
.await
.data
.lidarr_data
.artists
.set_items(vec![artist()]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::Releases(releases_vec) = network
.handle_lidarr_event(LidarrEvent::GetDiscographyReleases(1))
.await
.unwrap()
else {
panic!("Expected Releases")
};
mock.assert_async().await;
assert_eq!(
app.lock().await.data.lidarr_data.discography_releases.items,
vec![expected_filtered_lidarr_release]
);
assert_eq!(releases_vec, expected_raw_lidarr_releases);
}
#[tokio::test]
async fn test_handle_toggle_artist_monitoring_event() {
let artist_json = json!({
@@ -5,7 +5,7 @@ use serde_json::{Value, json};
use crate::models::Route;
use crate::models::lidarr_models::{
AddArtistBody, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, LidarrCommandBody,
LidarrHistoryItem,
LidarrHistoryItem, LidarrRelease,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::StatefulTable;
@@ -306,7 +306,7 @@ impl Network<'_, '_> {
Route::Lidarr(ActiveLidarrBlock::ArtistHistorySortPrompt, _)
);
let artist_history = app.data.lidarr_data.artist_history.get_or_insert_default();
let artist_history = &mut app.data.lidarr_data.artist_history;
if !is_sorting {
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
@@ -317,6 +317,39 @@ impl Network<'_, '_> {
.await
}
pub(in crate::network::lidarr_network) async fn get_artist_discography_releases(
&mut self,
artist_id: i64,
) -> Result<Vec<LidarrRelease>> {
let event = LidarrEvent::GetDiscographyReleases(artist_id);
info!("Fetching discography releases for artist with ID: {artist_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("artistId={artist_id}")),
)
.await;
self
.handle_request::<(), Vec<LidarrRelease>>(request_props, |release_vec, mut app| {
let artist_releases_vec = release_vec
.into_iter()
.filter(|release| release.discography)
.collect();
app
.data
.lidarr_data
.discography_releases
.set_items(artist_releases_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn edit_artist(
&mut self,
mut edit_artist_params: EditArtistParams,
@@ -0,0 +1,34 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::LidarrReleaseDownloadBody;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use serde_json::json;
#[tokio::test]
async fn test_handle_download_lidarr_release_event_uses_provided_params() {
let params = LidarrReleaseDownloadBody {
guid: "1234".to_owned(),
indexer_id: 2,
};
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"guid": "1234",
"indexerId": 2,
}))
.returns(json!({}))
.build_for(LidarrEvent::DownloadRelease(params.clone()))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::DownloadRelease(params))
.await;
mock.assert_async().await;
assert_ok!(result);
}
}
+35
View File
@@ -1,2 +1,37 @@
use crate::models::lidarr_models::LidarrReleaseDownloadBody;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use log::info;
use serde_json::Value;
mod albums;
mod artists;
#[cfg(test)]
#[path = "lidarr_library_network_tests.rs"]
mod lidarr_library_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn download_lidarr_release(
&mut self,
lidarr_release_download_body: LidarrReleaseDownloadBody,
) -> Result<Value> {
let event = LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody::default());
info!("Downloading Lidarr release with params: {lidarr_release_download_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Post,
Some(lidarr_release_download_body),
None,
None,
)
.await;
self
.handle_request::<LidarrReleaseDownloadBody, Value>(request_props, |_, _| ())
.await
}
}
@@ -4,8 +4,8 @@ pub mod test_utils {
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, LidarrHistoryData,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, LidarrTaskName,
Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrRelease, LidarrTask,
LidarrTaskName, Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus,
};
use crate::models::servarr_models::IndexerSettings;
use crate::models::servarr_models::{
@@ -377,4 +377,51 @@ pub mod test_utils {
* Fixed bug 2"
))
}
pub fn rejections() -> Vec<String> {
vec![
"Unknown quality profile".to_owned(),
"Release is already mapped".to_owned(),
]
}
pub fn torrent_release() -> LidarrRelease {
LidarrRelease {
guid: "1234".to_owned(),
protocol: "torrent".to_owned(),
age: 1,
title: HorizontallyScrollableText::from("Test Release"),
discography: false,
artist_name: Some("Alex".to_owned()),
album_title: Some("Something".to_owned()),
indexer: "kickass torrents".to_owned(),
indexer_id: 2,
size: 1234,
rejected: true,
rejections: Some(rejections()),
seeders: Some(Number::from(2)),
leechers: Some(Number::from(1)),
quality: quality_wrapper(),
}
}
pub fn usenet_release() -> LidarrRelease {
LidarrRelease {
guid: "1234".to_owned(),
protocol: "usenet".to_owned(),
age: 1,
title: HorizontallyScrollableText::from("Test Release"),
discography: false,
artist_name: Some("Alex".to_owned()),
album_title: Some("Something".to_owned()),
indexer: "DrunkenSlug".to_owned(),
indexer_id: 1,
size: 1234,
rejected: true,
rejections: Some(rejections()),
seeders: None,
leechers: None,
quality: quality_wrapper(),
}
}
}
@@ -124,6 +124,17 @@ mod tests {
assert_str_eq!(event.resource(), "/rootfolder");
}
#[rstest]
fn test_resource_release(
#[values(
LidarrEvent::GetDiscographyReleases(0),
LidarrEvent::DownloadRelease(Default::default())
)]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/release");
}
#[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
#[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")]
+13 -2
View File
@@ -3,8 +3,8 @@ use log::info;
use super::{NetworkEvent, NetworkResource};
use crate::models::lidarr_models::{
AddArtistBody, AddLidarrRootFolderBody, DeleteParams, EditArtistParams, LidarrSerdeable,
LidarrTaskName, MetadataProfile,
AddArtistBody, AddLidarrRootFolderBody, DeleteParams, EditArtistParams,
LidarrReleaseDownloadBody, LidarrSerdeable, LidarrTaskName, MetadataProfile,
};
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings, QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
@@ -35,6 +35,7 @@ pub enum LidarrEvent {
DeleteIndexer(i64),
DeleteRootFolder(i64),
DeleteTag(i64),
DownloadRelease(LidarrReleaseDownloadBody),
EditArtist(EditArtistParams),
EditAllIndexerSettings(IndexerSettings),
EditIndexer(EditIndexerParams),
@@ -43,6 +44,7 @@ pub enum LidarrEvent {
GetArtistHistory(i64),
GetAllIndexerSettings,
GetArtistDetails(i64),
GetDiscographyReleases(i64),
GetDiskSpace,
GetDownloads(u64),
GetHistory(u64),
@@ -96,6 +98,7 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue",
LidarrEvent::GetHistory(_) => "/history",
LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
LidarrEvent::GetDiscographyReleases(_) | LidarrEvent::DownloadRelease(_) => "/release",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::GetIndexers | LidarrEvent::DeleteIndexer(_) | LidarrEvent::EditIndexer(_) => {
"/indexer"
@@ -169,6 +172,10 @@ impl Network<'_, '_> {
.delete_lidarr_tag(tag_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::DownloadRelease(lidarr_release_download_body) => self
.download_lidarr_release(lidarr_release_download_body)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetAlbums(artist_id) => {
self.get_albums(artist_id).await.map(LidarrSerdeable::from)
}
@@ -184,6 +191,10 @@ impl Network<'_, '_> {
.get_album_details(album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetDiscographyReleases(artist_id) => self
.get_artist_discography_releases(artist_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from),
LidarrEvent::GetDownloads(count) => self
.get_lidarr_downloads(count)
@@ -370,6 +370,11 @@ impl Network<'_, '_> {
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
}
let episode_releases_vec = release_vec
.into_iter()
.filter(|release| !release.full_season)
.collect();
if app
.data
.sonarr_data
@@ -398,7 +403,7 @@ impl Network<'_, '_> {
.as_mut()
.unwrap()
.episode_releases
.set_items(release_vec);
.set_items(episode_releases_vec);
})
.await
}
@@ -4,7 +4,7 @@ mod tests {
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::sonarr_models::{
DownloadRecord, DownloadStatus, Episode, MonitorEpisodeBody, Season, Series, SonarrHistoryItem,
SonarrHistoryWrapper, SonarrSerdeable,
SonarrHistoryWrapper, SonarrRelease, SonarrSerdeable,
};
use crate::models::stateful_table::SortOption;
use crate::network::NetworkResource;
@@ -1067,21 +1067,53 @@ mod tests {
#[tokio::test]
async fn test_handle_get_episode_releases_event() {
let release_json = json!([{
"guid": "1234",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"languages": [ { "id": 1, "name": "English" } ],
"quality": { "quality": { "name": "Bluray-1080p" }}
}]);
let release_json = json!([
{
"guid": "1234",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"languages": [ { "id": 1, "name": "English" } ],
"quality": { "quality": { "name": "Bluray-1080p" }},
"fullSeason": true
},
{
"guid": "4567",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"languages": [ { "id": 1, "name": "English" } ],
"quality": { "quality": { "name": "Bluray-1080p" }},
}
]);
let expected_filtered_sonarr_release = SonarrRelease {
guid: "4567".to_owned(),
..torrent_release()
};
let expected_raw_sonarr_releases = vec![
SonarrRelease {
full_season: true,
..torrent_release()
},
SonarrRelease {
guid: "4567".to_owned(),
..torrent_release()
},
];
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(release_json)
.query("episodeId=1")
@@ -1124,28 +1156,60 @@ mod tests {
.unwrap()
.episode_releases
.items,
vec![torrent_release()]
vec![expected_filtered_sonarr_release]
);
assert_eq!(releases_vec, vec![torrent_release()]);
assert_eq!(releases_vec, expected_raw_sonarr_releases);
}
#[tokio::test]
async fn test_handle_get_episode_releases_event_empty_episode_details_modal() {
let release_json = json!([{
"guid": "1234",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"languages": [ { "id": 1, "name": "English" } ],
"quality": { "quality": { "name": "Bluray-1080p" }}
}]);
let release_json = json!([
{
"guid": "1234",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"languages": [ { "id": 1, "name": "English" } ],
"quality": { "quality": { "name": "Bluray-1080p" }},
"fullSeason": true
},
{
"guid": "4567",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"languages": [ { "id": 1, "name": "English" } ],
"quality": { "quality": { "name": "Bluray-1080p" }},
}
]);
let expected_filtered_sonarr_release = SonarrRelease {
guid: "4567".to_owned(),
..torrent_release()
};
let expected_raw_sonarr_releases = vec![
SonarrRelease {
full_season: true,
..torrent_release()
},
SonarrRelease {
guid: "4567".to_owned(),
..torrent_release()
},
];
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(release_json)
.query("episodeId=1")
@@ -1179,9 +1243,9 @@ mod tests {
.unwrap()
.episode_releases
.items,
vec![torrent_release()]
vec![expected_filtered_sonarr_release]
);
assert_eq!(releases_vec, vec![torrent_release()]);
assert_eq!(releases_vec, expected_raw_sonarr_releases);
}
#[tokio::test]
@@ -32,6 +32,6 @@ mod tests {
.await;
mock.assert_async().await;
assert!(result.is_ok());
assert_ok!(result);
}
}
+241 -80
View File
@@ -9,11 +9,12 @@ use regex::Regex;
use crate::app::App;
use crate::models::Route;
use crate::models::lidarr_models::{Album, LidarrHistoryItem};
use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::{ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock};
use crate::ui::lidarr_ui::library::delete_album_ui::DeleteAlbumUi;
use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details;
use crate::ui::styles::{ManagarrStyle, secondary_style};
use crate::ui::utils::decorate_peer_style;
use crate::ui::utils::{
borderless_block, get_width_from_percentage, layout_block_top_border, title_block,
};
@@ -26,6 +27,7 @@ use crate::ui::{DrawUi, draw_popup, draw_tabs};
use crate::utils::convert_to_gb;
use ratatui::layout::Alignment;
use ratatui::text::Text;
use serde_json::Number;
#[cfg(test)]
#[path = "artist_details_ui_tests.rs"]
@@ -105,6 +107,9 @@ impl DrawUi for ArtistDetailsUi {
f.area(),
);
}
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
draw_manual_artist_search_confirm_prompt(f, app);
}
_ => (),
}
};
@@ -241,6 +246,7 @@ fn draw_artist_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match active_lidarr_block {
ActiveLidarrBlock::ArtistDetails => draw_albums_table(f, app, area),
ActiveLidarrBlock::ArtistHistory => draw_artist_history_table(f, app, area),
ActiveLidarrBlock::ManualArtistSearch => draw_artist_releases(f, app, area),
_ => (),
}
}
@@ -338,98 +344,98 @@ fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
}
fn draw_artist_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match app.data.lidarr_data.artist_history.as_ref() {
Some(artist_history) if !app.is_loading => {
let current_selection = if artist_history.is_empty() {
LidarrHistoryItem::default()
} else {
artist_history.current_selection().clone()
if !app.is_loading {
let current_selection = if app.data.lidarr_data.artist_history.is_empty() {
LidarrHistoryItem::default()
} else {
app
.data
.lidarr_data
.artist_history
.current_selection()
.clone()
};
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let history_row_mapping = |history_item: &LidarrHistoryItem| {
let LidarrHistoryItem {
source_title,
quality,
event_type,
date,
..
} = history_item;
source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40),
current_selection == *history_item,
app.ui_scroll_tick_count == 0,
);
Row::new(vec![
Cell::from(source_title.to_string()),
Cell::from(event_type.to_string()),
Cell::from(quality.quality.name.to_owned()),
Cell::from(date.to_string()),
])
.primary()
};
let history_table = ManagarrTable::new(
Some(&mut app.data.lidarr_data.artist_history),
history_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_lidarr_block == ActiveLidarrBlock::ArtistHistorySortPrompt)
.searching(active_lidarr_block == ActiveLidarrBlock::SearchArtistHistory)
.search_produced_empty_results(
active_lidarr_block == ActiveLidarrBlock::SearchArtistHistoryError,
)
.filtering(active_lidarr_block == ActiveLidarrBlock::FilterArtistHistory)
.filter_produced_empty_results(
active_lidarr_block == ActiveLidarrBlock::FilterArtistHistoryError,
)
.headers(["Source Title", "Event Type", "Quality", "Date"])
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(20),
Constraint::Percentage(15),
Constraint::Percentage(25),
]);
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let history_row_mapping = |history_item: &LidarrHistoryItem| {
let LidarrHistoryItem {
source_title,
quality,
event_type,
date,
..
} = history_item;
source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40),
current_selection == *history_item,
app.ui_scroll_tick_count == 0,
);
Row::new(vec![
Cell::from(source_title.to_string()),
Cell::from(event_type.to_string()),
Cell::from(quality.quality.name.to_owned()),
Cell::from(date.to_string()),
])
.primary()
};
let mut artist_history_table = app
.data
.lidarr_data
.artist_history
.as_mut()
.expect("artist_history must be populated");
let history_table =
ManagarrTable::new(Some(&mut artist_history_table), history_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_lidarr_block == ActiveLidarrBlock::ArtistHistorySortPrompt)
.searching(active_lidarr_block == ActiveLidarrBlock::SearchArtistHistory)
.search_produced_empty_results(
active_lidarr_block == ActiveLidarrBlock::SearchArtistHistoryError,
)
.filtering(active_lidarr_block == ActiveLidarrBlock::FilterArtistHistory)
.filter_produced_empty_results(
active_lidarr_block == ActiveLidarrBlock::FilterArtistHistoryError,
)
.headers(["Source Title", "Event Type", "Quality", "Date"])
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(20),
Constraint::Percentage(15),
Constraint::Percentage(25),
]);
if [
ActiveLidarrBlock::SearchArtistHistory,
ActiveLidarrBlock::FilterArtistHistory,
]
.contains(&active_lidarr_block)
{
history_table.show_cursor(f, area);
}
f.render_widget(history_table, area);
if [
ActiveLidarrBlock::SearchArtistHistory,
ActiveLidarrBlock::FilterArtistHistory,
]
.contains(&active_lidarr_block)
{
history_table.show_cursor(f, area);
}
f.render_widget(history_table, area);
}
_ => f.render_widget(
} else {
f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.lidarr_data.albums.is_empty(),
layout_block_top_border(),
),
area,
),
);
}
}
fn draw_artist_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let current_selection =
if let Some(artist_history_items) = app.data.lidarr_data.artist_history.as_ref() {
if artist_history_items.is_empty() {
LidarrHistoryItem::default()
} else {
artist_history_items.current_selection().clone()
}
} else {
LidarrHistoryItem::default()
};
let current_selection = if app.data.lidarr_data.artist_history.is_empty() {
LidarrHistoryItem::default()
} else {
app
.data
.lidarr_data
.artist_history
.current_selection()
.clone()
};
let line_vec = create_history_event_details(current_selection);
let text = Text::from(line_vec);
@@ -441,3 +447,158 @@ fn draw_artist_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>)
f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area());
}
fn draw_artist_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let (current_selection, is_empty) = if app.data.lidarr_data.discography_releases.is_empty() {
(LidarrRelease::default(), true)
} else {
(
app
.data
.lidarr_data
.discography_releases
.current_selection()
.clone(),
app.data.lidarr_data.discography_releases.is_empty(),
)
};
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let release_row_mapping = |release: &LidarrRelease| {
let LidarrRelease {
protocol,
age,
title,
indexer,
size,
rejected,
seeders,
leechers,
quality,
..
} = release;
let age = format!("{age} days");
title.scroll_left_or_reset(
get_width_from_percentage(area, 35),
current_selection == *release
&& active_lidarr_block != ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
app.ui_scroll_tick_count == 0,
);
let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() {
Text::from("")
} else {
let seeders = seeders
.clone()
.unwrap_or(Number::from(0u64))
.as_u64()
.unwrap();
let leechers = leechers
.clone()
.unwrap_or(Number::from(0u64))
.as_u64()
.unwrap();
decorate_peer_style(
seeders,
leechers,
Text::from(format!("{seeders} / {leechers}")),
)
};
let quality_name = quality.quality.name.clone();
Row::new(vec![
Cell::from(protocol.clone()),
Cell::from(age),
Cell::from(rejected_str),
Cell::from(title.to_string()),
Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")),
Cell::from(peers),
Cell::from(quality_name),
])
.primary()
};
let mut release_table = &mut app.data.lidarr_data.discography_releases;
let artist_release_table = ManagarrTable::new(Some(&mut release_table), release_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading || is_empty)
.sorting(active_lidarr_block == ActiveLidarrBlock::ManualArtistSearchSortPrompt)
.headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Quality",
])
.constraints([
Constraint::Length(9),
Constraint::Length(10),
Constraint::Length(5),
Constraint::Percentage(35),
Constraint::Percentage(15),
Constraint::Length(12),
Constraint::Length(12),
Constraint::Percentage(10),
]);
f.render_widget(artist_release_table, area);
}
}
fn draw_manual_artist_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) {
let current_selection = app
.data
.lidarr_data
.discography_releases
.current_selection();
let title = if current_selection.rejected {
"Download Rejected Release"
} else {
"Download Release"
};
let prompt = if current_selection.rejected {
format!(
"Do you really want to download the rejected release: {}?",
&current_selection.title.text
)
} else {
format!(
"Do you want to download the release: {}?",
&current_selection.title.text
)
};
if current_selection.rejected {
let mut lines_vec = vec![Line::from("Rejection reasons: ".primary().bold())];
let mut rejections_spans = current_selection
.rejections
.clone()
.unwrap_or_default()
.iter()
.map(|item| Line::from(format!("{item}").primary().bold()))
.collect::<Vec<Line<'_>>>();
lines_vec.append(&mut rejections_spans);
let content_paragraph = Paragraph::new(lines_vec)
.block(borderless_block())
.wrap(Wrap { trim: false })
.left_aligned();
let confirmation_prompt = ConfirmationPrompt::new()
.title(title)
.prompt(&prompt)
.content(content_paragraph)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(Popup::new(confirmation_prompt).size(Size::Small), f.area());
} else {
let confirmation_prompt = ConfirmationPrompt::new()
.title(title)
.prompt(&prompt)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
}
@@ -38,18 +38,23 @@ mod tests {
#[rstest]
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
#[case(ActiveLidarrBlock::ArtistHistory, 1)]
#[case(ActiveLidarrBlock::ManualArtistSearch, 2)]
#[case(ActiveLidarrBlock::SearchAlbums, 0)]
#[case(ActiveLidarrBlock::SearchAlbumsError, 0)]
#[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, 0)]
#[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, 1)]
#[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, 2)]
#[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, 0)]
#[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, 1)]
#[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, 2)]
#[case(ActiveLidarrBlock::SearchArtistHistory, 1)]
#[case(ActiveLidarrBlock::SearchArtistHistoryError, 1)]
#[case(ActiveLidarrBlock::FilterArtistHistory, 1)]
#[case(ActiveLidarrBlock::FilterArtistHistoryError, 1)]
#[case(ActiveLidarrBlock::ArtistHistorySortPrompt, 1)]
#[case(ActiveLidarrBlock::ArtistHistoryDetails, 1)]
#[case(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt, 2)]
#[case(ActiveLidarrBlock::ManualArtistSearchSortPrompt, 2)]
fn test_artist_details_ui_renders(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
@@ -71,6 +76,7 @@ mod tests {
#[rstest]
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
#[case(ActiveLidarrBlock::ArtistHistory, 1)]
#[case(ActiveLidarrBlock::ManualArtistSearch, 2)]
fn test_artist_details_ui_renders_artist_details_loading(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
@@ -94,12 +100,14 @@ mod tests {
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
#[case(ActiveLidarrBlock::ArtistHistory, 1)]
#[case(ActiveLidarrBlock::ArtistHistoryDetails, 1)]
#[case(ActiveLidarrBlock::ManualArtistSearch, 2)]
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.data.lidarr_data.discography_releases = StatefulTable::default();
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.artist_info_tabs.set_index(index);
+20 -5
View File
@@ -280,19 +280,34 @@ mod tests {
insta::assert_snapshot!(output);
}
#[test]
fn test_library_ui_renders_edit_artist_over_artist_details() {
#[rstest]
#[case(ActiveLidarrBlock::ArtistDetails, 0)]
#[case(ActiveLidarrBlock::ArtistHistory, 1)]
#[case(ActiveLidarrBlock::ManualArtistSearch, 2)]
fn test_library_ui_renders_edit_artist_over_artist_details(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
) {
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::EditArtistPrompt,
Some(active_lidarr_block),
)
.into(),
);
app.data.lidarr_data.artist_info_tabs.set_index(index);
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);
insta::assert_snapshot!(
format!("edit_artist_renders_over_{active_lidarr_block}"),
output
);
}
#[test]
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 ╭─────────────────────────────────── Details ───────────────────────────────────╮ │
│Size on Disk: 0.00 GB │Source Title: Test source title │ │
│╭ Artist Details ─────────────│Event Type: grabbed │───────────────────────────────╮│
││ Albums │ History │Quality: Lossless │ ││
││ Albums │ History │ Manual Sear│Quality: Lossless │ ││
││───────────────────────────────│Date: 2023-01-01 00:00:00 UTC │───────────────────────────────││
││ Source Title ▼ │Indexer: │ ││
││=> Test source title │NZB Info URL: │-01-01 00:00:00 UTC ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Title Event Type Quality Date ││
││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Title ▼ Event Type Quality Date ││
││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │
│Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │
│╭ Artist Details ───────────────────────│ for all monitored album(s) for the artist: Alex? │───────────────────────────────────────────╮│
││ Albums │ History │ │ ││
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │
│Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │
│╭ Artist Details ───────────────────────│ for all monitored album(s) for the artist: Alex? │───────────────────────────────────────────╮│
││ Albums │ History │ │ ││
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Source Title ▼ │ │ Date ││
││=> Test source title │ │ 2023-01-01 00:00:00 UTC ││
@@ -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│ │
│╭ Artist Details ───────────────────────│ for all monitored album(s) for the artist: Alex? │───────────────────────────────────────────╮│
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
││=> torrent 1 days ⛔ Test Releas│ │ 0.0 GB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Releas│ │ 0.0 GB Lossless ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
││ ││ Yes ││ No ││ ││
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
││ ╰───────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Title ▼ Event Type Quality Date ││
││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Title ▼ Event Type Quality Date ││
││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││
@@ -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 ╭───────────────── Download Rejected Release ──────────────────╮ │
│Tracks: 15/15 │ Do you really want to download the rejected release: Test │ │
│Size on Disk: 0.00 GB │ Release? │ │
│╭ Artist Details ──────────────────────│ │───────────────────────────────────────╮│
││ Albums │ History │ Manual Search │ │ ││
││────────────────────────────────────────│Rejection reasons: │───────────────────────────────────────││
││ Source ▼ Age ⛔ Title │• Unknown quality profile │e Peers Quality ││
││=> torrent 1 days ⛔ Test Relea│• Release is already mapped │ GB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Relea│ │ GB Lossless ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭──────────────────────────────╮╭──────────────────────────────╮│ ││
││ ││ Yes ││ No ││ ││
││ │╰──────────────────────────────╯╰──────────────────────────────╯│ ││
││ ╰────────────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Age ⛔ Title Indexer Size Peers Quality ││
││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless ││
││ ││
││ ││
││ ││
││ ╭───────────────────────────╮ ││
││ │Something │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ ╰───────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -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 │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source ▼ Age ⛔ Title Indexer Size Peers Quality ││
││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Title ▼ Event Type Quality Date ││
││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Title ▼ Event Type Quality Date ││
││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││
@@ -21,7 +21,7 @@ expression: output
│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 Details ───────────────────────│ artist: Alex? │───────────────────────────────────────────╮│
││ Albums │ History │ │ ││
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
@@ -21,7 +21,7 @@ expression: output
│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 Details ───────────────────────│ artist: Alex? │───────────────────────────────────────────╮│
││ Albums │ History │ │ ││
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Source Title ▼ │ │ Date ││
││=> Test source title │ │ 2023-01-01 00:00:00 UTC ││
@@ -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 Details ───────────────────────│ artist: Alex? │───────────────────────────────────────────╮│
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
││=> torrent 1 days ⛔ Test Releas│ │ 0.0 GB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Releas│ │ 0.0 GB Lossless ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭────────────────────────────╮╭───────────────────────────╮│ ││
││ ││ Yes ││ No ││ ││
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
││ ╰───────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │
│Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │
│╭ Artist Details ───────────────────────│ for all monitored album(s) for the artist: Alex? │───────────────────────────────────────────╮│
││ Albums │ History │ │ ││
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 ╭───────────────────── Delete Album ──────────────────────╮ │
│Size on Disk: 0.00 GB │ Do you really want to delete the album: │ │
│╭ Artist Details ───────────────────────│ Test Album? │───────────────────────────────────────────╮│
││ Albums │ History │ │ ││
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ ╭───╮ │ Release Date Size ││
││=> 🏷 Test Album │ Delete Album Files: │ ✔ │ │ 2023-01-01 0.00 GB ││
@@ -21,7 +21,7 @@ expression: output
│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 Details ───────────────────────│ artist: Alex? │───────────────────────────────────────────╮│
││ Albums │ History │ │ ││
││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ ││
││ ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Title ▼ Event Type Quality Date ││
││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 ╭─────────────────────────────────── Details ───────────────────────────────────╮ │
│Size on Disk: 0.00 GB │Source Title: Test source title │ │
│╭ Artist Details ─────────────│Event Type: grabbed │───────────────────────────────╮│
││ Albums │ History │Quality: Lossless │ ││
││ Albums │ History │ Manual Sear│Quality: Lossless │ ││
││───────────────────────────────│Date: 2023-01-01 00:00:00 UTC │───────────────────────────────││
││ Source Title ▼ │Indexer: │ ││
││=> Test source title │NZB Info URL: │-01-01 00:00:00 UTC ││
@@ -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 │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ ││
││ ││
││ Loading ... ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ ││
││ ││
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ ││
││ ││
@@ -0,0 +1,52 @@
---
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
expression: output
---
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │
│Overview: some interesting description of the artist │
│Disambiguation: American pianist │
│Type: Person │
│Status: Continuing │
│Genres: soundtrack │
│Rating: 84% │
│Path: /nfs/music/test-artist │
│Quality Profile: Lossless │
│Metadata Profile: Standard │
│Monitored: Yes │
│Albums: 1 │
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ ││
││ ││
││ Loading ... ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -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: Ame╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ │
│Type: Person │ │ │
│Status: Continuing │ │ │
│Genres: soundtrack │ │ │
│Rating: 84% │ │ │
│Path: /nfs/music/te│ │ │
│Quality Profile: Lo│ │ │
│Metadata Profile: S│ │ │
│Monitored: Yes │ │ │
│Albums: 1 │ ╭───╮ │ │
│Tracks: 15/15 │ Monitored: │ ✔ │ │ │
│Size on Disk: 0.00 │ ╰───╯ │ │
│╭ Artist Details │ ╭─────────────────────────────────────────────────╮ │────────────────────╮│
││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││
││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││
││ Monitored Titl│ ╭─────────────────────────────────────────────────╮ │ize ││
││=> 🏷 Test│ Quality Profile: │Lossless ▼ │ │.00 GB ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Metadata Profile: │Standard ▼ │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Path: │/nfs/music │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Tags: │alex │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ ││
││ ││ Save ││ Cancel ││ ││
││ │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ ││
││ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -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: Ame╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ │
│Type: Person │ │ │
│Status: Continuing │ │ │
│Genres: soundtrack │ │ │
│Rating: 84% │ │ │
│Path: /nfs/music/te│ │ │
│Quality Profile: Lo│ │ │
│Metadata Profile: S│ │ │
│Monitored: Yes │ │ │
│Albums: 1 │ ╭───╮ │ │
│Tracks: 15/15 │ Monitored: │ ✔ │ │ │
│Size on Disk: 0.00 │ ╰───╯ │ │
│╭ Artist Details │ ╭─────────────────────────────────────────────────╮ │────────────────────╮│
││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││
││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││
││ Source Title ▼ │ ╭─────────────────────────────────────────────────╮ │ ││
││=> Test source tit│ Quality Profile: │Lossless ▼ │ │0:00 UTC ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Metadata Profile: │Standard ▼ │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Path: │/nfs/music │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Tags: │alex │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ ││
││ ││ Save ││ Cancel ││ ││
││ │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ ││
││ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -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: Ame╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ │
│Type: Person │ │ │
│Status: Continuing │ │ │
│Genres: soundtrack │ │ │
│Rating: 84% │ │ │
│Path: /nfs/music/te│ │ │
│Quality Profile: Lo│ │ │
│Metadata Profile: S│ │ │
│Monitored: Yes │ │ │
│Albums: 1 │ ╭───╮ │ │
│Tracks: 15/15 │ Monitored: │ ✔ │ │ │
│Size on Disk: 0.00 │ ╰───╯ │ │
│╭ Artist Details │ ╭─────────────────────────────────────────────────╮ │────────────────────╮│
││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││
││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││
││ Source ▼ Age │ ╭─────────────────────────────────────────────────╮ │ Quality ││
││=> torrent 1 day│ Quality Profile: │Lossless ▼ │ │ Lossless ││
││ usenet 1 day│ ╰─────────────────────────────────────────────────╯ │ Lossless ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Metadata Profile: │Standard ▼ │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Path: │/nfs/music │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Tags: │alex │ │ ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ ││
││ ││ Save ││ Cancel ││ ││
││ │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ ││
││ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -21,7 +21,7 @@ expression: output
│Tracks: 15/15 │
│Size on Disk: 0.00 GB │
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Albums │ History ││
││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││