Lidarr support #1

Merged
Dark-Alex-17 merged 61 commits from lidarr into main 2026-01-21 21:30:47 +00:00
54 changed files with 3462 additions and 329 deletions
Showing only changes of commit eff1a901eb - Show all commits
+35 -3
View File
@@ -8,6 +8,7 @@ use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS,
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS,
TRACK_DETAILS_BLOCKS,
};
#[cfg(test)]
@@ -103,8 +104,8 @@ pub static ALBUM_DETAILS_CONTEXT_CLUES: [ContextClue; 6] = [
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
(DEFAULT_KEYBINDINGS.submit, "track details"),
(DEFAULT_KEYBINDINGS.delete, "delete track"),
];
pub static ALBUM_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
@@ -137,6 +138,26 @@ pub static MANUAL_ALBUM_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static TRACK_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static TRACK_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub(in crate::app) struct LidarrContextClueProvider;
impl ContextClueProvider for LidarrContextClueProvider {
@@ -156,9 +177,20 @@ impl ContextClueProvider for LidarrContextClueProvider {
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.expect("album_details_modal is empty")
.album_details_tabs
.get_active_route_contextual_help(),
_ if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("album_details_modal is empty")
.track_details_modal
.as_ref()
.expect("track_details_modal is empty")
.track_details_tabs
.get_active_route_contextual_help(),
ActiveLidarrBlock::AddArtistSearchInput
| ActiveLidarrBlock::AddArtistEmptySearchResults
| ActiveLidarrBlock::TestAllIndexers
+84 -4
View File
@@ -10,13 +10,13 @@ mod tests {
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ALBUM_DETAILS_CONTEXT_CLUES,
ALBUM_HISTORY_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES,
ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES, TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS,
INDEXER_SETTINGS_BLOCKS, LidarrData,
};
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::servarr_data::lidarr::modals::{AlbumDetailsModal, TrackDetailsModal};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use rstest::rstest;
@@ -266,11 +266,11 @@ mod tests {
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "episode details")
&(DEFAULT_KEYBINDINGS.submit, "track details")
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete episode")
&(DEFAULT_KEYBINDINGS.delete, "delete track")
);
assert_none!(album_details_context_clues_iter.next());
}
@@ -278,6 +278,7 @@ mod tests {
#[test]
fn test_album_history_context_clues() {
let mut album_history_context_clues_iter = ALBUM_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(
@@ -348,6 +349,58 @@ mod tests {
assert_none!(manual_album_search_context_clues_iter.next());
}
#[test]
fn test_track_details_context_clues() {
let mut track_details_context_clues_iter = TRACK_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
track_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
track_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(track_details_context_clues_iter.next());
}
#[test]
fn test_track_history_context_clues() {
let mut track_history_context_clues_iter = TRACK_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(track_history_context_clues_iter.next());
}
#[rstest]
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::ArtistHistory, &ARTIST_HISTORY_CONTEXT_CLUES)]
@@ -391,6 +444,33 @@ mod tests {
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[rstest]
#[case(0, ActiveLidarrBlock::TrackDetails, &TRACK_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::TrackHistory, &TRACK_HISTORY_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_track_details_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
let mut track_details_modal = TrackDetailsModal::default();
track_details_modal.track_details_tabs.set_index(index);
let album_details_modal = AlbumDetailsModal {
track_details_modal: Some(track_details_modal),
..AlbumDetailsModal::default()
};
let lidarr_data = LidarrData {
album_details_modal: Some(album_details_modal),
..LidarrData::default()
};
app.data.lidarr_data = lidarr_data;
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[test]
fn test_lidarr_context_clue_provider_artists_block() {
let mut app = App::test_default();
+69 -1
View File
@@ -7,7 +7,9 @@ mod tests {
use crate::models::servarr_models::Indexer;
use crate::network::NetworkEvent;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
album, artist, track,
};
use pretty_assertions::{assert_eq, assert_str_eq};
use tokio::sync::mpsc;
@@ -464,6 +466,46 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_track_details_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default_fully_populated();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackDetails)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTrackDetails(1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_track_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default_fully_populated();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackHistory)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTrackHistory(1, 1, 1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_check_for_lidarr_prompt_action_no_prompt_confirm() {
let mut app = App::test_default();
@@ -684,6 +726,32 @@ mod tests {
assert_eq!(app.extract_artist_id().await, 1);
}
#[tokio::test]
async fn test_extract_album_id() {
let mut app = App::test_default();
app.data.lidarr_data.albums.set_items(vec![album()]);
assert_eq!(app.extract_album_id().await, 1);
}
#[tokio::test]
async fn test_extract_track_id() {
let mut app = App::test_default();
let mut album_details_modal = AlbumDetailsModal::default();
album_details_modal.tracks.set_items(vec![track()]);
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
assert_eq!(app.extract_track_id().await, 1);
}
#[tokio::test]
#[should_panic(expected = "album_details_modal is empty")]
async fn test_extract_track_id_panics_when_album_details_modal_is_not_set() {
let app = App::test_default();
app.extract_track_id().await;
}
#[tokio::test]
async fn test_extract_lidarr_indexer_id() {
let mut app = App::test_default();
+29
View File
@@ -153,6 +153,23 @@ impl App<'_> {
.dispatch_network_event(LidarrEvent::GetUpdates.into())
.await;
}
ActiveLidarrBlock::TrackDetails => {
self
.dispatch_network_event(
LidarrEvent::GetTrackDetails(self.extract_track_id().await).into(),
)
.await;
}
ActiveLidarrBlock::TrackHistory => {
let artist_id = self.extract_artist_id().await;
let album_id = self.extract_album_id().await;
let track_id = self.extract_track_id().await;
self
.dispatch_network_event(
LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into(),
)
.await;
}
_ => (),
}
@@ -179,6 +196,18 @@ impl App<'_> {
self.data.lidarr_data.albums.current_selection().id
}
async fn extract_track_id(&self) -> i64 {
self
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("album_details_modal is empty")
.tracks
.current_selection()
.id
}
async fn extract_lidarr_indexer_id(&self) -> i64 {
self.data.lidarr_data.indexers.current_selection().id
}
+16
View File
@@ -44,6 +44,15 @@ pub enum LidarrGetCommand {
SecurityConfig,
#[command(about = "Get the system status")]
SystemStatus,
#[command(about = "Get detailed information for the track with the given ID")]
TrackDetails {
#[arg(
long,
help = "The Lidarr ID of the track whose details you wish to fetch",
required = true
)]
track_id: i64,
},
}
impl From<LidarrGetCommand> for Command {
@@ -115,6 +124,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHan
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::TrackDetails { track_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTrackDetails(track_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
@@ -106,6 +106,32 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_track_details_requires_track_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "track-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_track_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"get",
"track-details",
"--track-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
@@ -273,5 +299,31 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_track_details_command() {
let expected_track_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackDetails(expected_track_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_track_details_command = LidarrGetCommand::TrackDetails { track_id: 1 };
let result =
LidarrGetCommandHandler::with(&app_arc, get_track_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
+46 -2
View File
@@ -2,16 +2,18 @@ use std::sync::Arc;
use anyhow::Result;
use clap::{Subcommand, arg};
use serde_json::json;
use tokio::sync::Mutex;
use super::LidarrCommand;
use crate::models::Serdeable;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "list_command_handler_tests.rs"]
mod list_command_handler_tests;
@@ -89,6 +91,27 @@ pub enum LidarrListCommand {
Tags,
#[command(about = "List all Lidarr tasks")]
Tasks,
#[command(about = "Fetch all history events for the track with the given ID")]
TrackHistory {
#[arg(
long,
help = "The artist ID that the track belongs to",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The album ID that the track is a part of",
required = true
)]
album_id: i64,
#[arg(
long,
help = "The Lidarr ID of the track whose history you wish to fetch",
required = true
)]
track_id: i64,
},
#[command(
about = "List the tracks for the album that corresponds to the artist with the given ID"
)]
@@ -257,6 +280,27 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::TrackHistory {
artist_id,
album_id,
track_id,
} => {
match self
.network
.handle_network_event(LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(history_vec))) => {
let history_items_vec: Vec<LidarrHistoryItem> = history_vec
.into_iter()
.filter(|it| it.track_id == track_id)
.collect();
serde_json::to_string_pretty(&history_items_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
LidarrListCommand::Tracks {
artist_id,
album_id,
+136 -1
View File
@@ -225,6 +225,96 @@ mod tests {
assert_eq!(logs_command, expected_args);
}
#[test]
fn test_list_track_history_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--album-id",
"1",
"--track-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--track-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_requires_track_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_success() {
let expected_args = LidarrListCommand::TrackHistory {
artist_id: 1,
album_id: 1,
track_id: 1,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--album-id",
"1",
"--track-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(track_history_command))) =
result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(track_history_command, expected_args);
}
#[test]
fn test_list_tracks_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
@@ -325,6 +415,7 @@ mod tests {
use std::sync::Arc;
use mockall::predicate::eq;
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
@@ -332,8 +423,9 @@ mod tests {
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::lidarr_history_item;
use crate::{
app::App,
network::{MockNetworkTrait, NetworkEvent},
@@ -531,6 +623,49 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_track_history_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let expected_track_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackHistory(expected_artist_id, expected_album_id, expected_track_id)
.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(
vec![
lidarr_history_item(),
LidarrHistoryItem {
track_id: 2,
..lidarr_history_item()
},
],
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_track_history_command = LidarrListCommand::TrackHistory {
artist_id: expected_artist_id,
album_id: expected_album_id,
track_id: expected_track_id,
};
let result =
LidarrListCommandHandler::with(&app_arc, list_track_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&[lidarr_history_item()]).unwrap()
);
}
#[tokio::test]
async fn test_handle_list_tracks_command() {
let expected_artist_id = 1;
@@ -109,16 +109,20 @@ mod tests {
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
torrent_release, usenet_release,
};
use crate::network::{MockNetworkTrait, NetworkEvent};
use mockall::predicate::eq;
use serde_json::json;
use pretty_assertions::assert_str_eq;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_manual_album_search_command() {
let expected_releases = [torrent_release()];
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
@@ -129,9 +133,13 @@ mod tests {
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
torrent_release(),
LidarrRelease {
discography: true,
..usenet_release()
},
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_album_search_command = LidarrManualSearchCommand::Album {
@@ -148,10 +156,18 @@ mod tests {
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&expected_releases).unwrap()
);
}
#[tokio::test]
async fn test_manual_discography_search_command() {
let expected_releases = [LidarrRelease {
discography: true,
..usenet_release()
}];
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
@@ -161,9 +177,13 @@ mod tests {
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
torrent_release(),
LidarrRelease {
discography: true,
..usenet_release()
},
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_discography_search_command =
@@ -178,6 +198,10 @@ mod tests {
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&expected_releases).unwrap()
);
}
}
}
@@ -227,22 +227,22 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AlbumDetailsHandler<
fn handle_submit(&mut self) {
match self.active_lidarr_block {
// ActiveLidarrBlock::AlbumDetails
// if self.app.data.lidarr_data.album_details_modal.is_some()
// && !self
// .app
// .data
// .lidarr_data
// .album_details_modal
// .as_ref()
// .unwrap()
// .tracks
// .is_empty() =>
// {
// self
// .app
// .push_navigation_stack(ActiveLidarrBlock::TrackDetails.into())
// }
ActiveLidarrBlock::AlbumDetails
if self.app.data.lidarr_data.album_details_modal.is_some()
&& !self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.tracks
.is_empty() =>
{
self
.app
.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into())
}
ActiveLidarrBlock::AlbumHistory => self
.app
.push_navigation_stack(ActiveLidarrBlock::AlbumHistoryDetails.into()),
@@ -155,37 +155,37 @@ mod tests {
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
// #[test]
// fn test_album_details_submit() {
// let mut app = App::test_default_fully_populated();
// app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
//
// AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None)
// .handle();
//
// assert_navigation_pushed!(app, ActiveLidarrBlock::TrackDetails.into());
// }
#[test]
fn test_album_details_submit() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
// #[test]
// fn test_album_details_submit_no_op_on_empty_tracks_table() {
// let mut app = App::test_default_fully_populated();
// app
// .data
// .lidarr_data
// .album_details_modal
// .as_mut()
// .unwrap()
// .tracks = StatefulTable::default();
// app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
//
// AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None)
// .handle();
//
// assert_eq!(
// app.get_current_route(),
// ActiveLidarrBlock::AlbumDetails.into()
// );
// }
AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::TrackDetails.into());
}
#[test]
fn test_album_details_submit_no_op_on_empty_tracks_table() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.tracks = StatefulTable::default();
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::AlbumDetails.into()
);
}
#[test]
fn test_album_details_submit_no_op_when_not_ready() {
@@ -1,8 +1,6 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_handlers::history::history_sorting_options;
use crate::handlers::lidarr_handlers::library::album_details_handler::AlbumDetailsHandler;
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
@@ -26,7 +24,7 @@ pub struct ArtistDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
_context: Option<ActiveLidarrBlock>,
}
impl ArtistDetailsHandler<'_, '_> {
@@ -76,24 +74,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|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) => {
DeleteAlbumHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if AlbumDetailsHandler::accepts(self.active_lidarr_block) => {
AlbumDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ => self.handle_key_event(),
};
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
DeleteAlbumHandler::accepts(active_block)
|| AlbumDetailsHandler::accepts(active_block)
|| ARTIST_DETAILS_BLOCKS.contains(&active_block)
ARTIST_DETAILS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
@@ -104,13 +90,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
_context: Option<ActiveLidarrBlock>,
) -> ArtistDetailsHandler<'a, 'b> {
ArtistDetailsHandler {
key,
app,
active_lidarr_block: active_block,
context,
_context,
}
}
@@ -13,12 +13,11 @@ mod tests {
ArtistDetailsHandler, releases_sorting_options,
};
use crate::models::HorizontallyScrollableText;
use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease};
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::{
ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
};
use crate::models::servarr_models::{Quality, QualityWrapper};
use crate::test_handler_delegation;
mod test_handle_delete {
use super::*;
@@ -812,12 +811,8 @@ mod tests {
#[test]
fn test_artist_details_handler_accepts() {
let mut artist_details_blocks = ARTIST_DETAILS_BLOCKS.clone().to_vec();
artist_details_blocks.extend(DELETE_ALBUM_BLOCKS);
artist_details_blocks.extend(ALBUM_DETAILS_BLOCKS);
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if artist_details_blocks.contains(&active_lidarr_block) {
if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) {
assert!(ArtistDetailsHandler::accepts(active_lidarr_block));
} else {
assert!(!ArtistDetailsHandler::accepts(active_lidarr_block));
@@ -977,58 +972,6 @@ mod tests {
assert!(handler.is_ready());
}
#[test]
fn test_delegates_delete_album_blocks_to_delete_album_handler() {
let mut app = App::test_default();
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ArtistDetails.into()
);
}
#[rstest]
fn test_delegates_album_details_blocks_to_album_details_handler(
#[values(
ActiveLidarrBlock::AlbumDetails,
ActiveLidarrBlock::AlbumHistory,
ActiveLidarrBlock::SearchTracks,
ActiveLidarrBlock::SearchTracksError,
ActiveLidarrBlock::AutomaticallySearchAlbumPrompt,
ActiveLidarrBlock::SearchAlbumHistory,
ActiveLidarrBlock::SearchAlbumHistoryError,
ActiveLidarrBlock::FilterAlbumHistory,
ActiveLidarrBlock::FilterAlbumHistoryError,
ActiveLidarrBlock::AlbumHistorySortPrompt,
ActiveLidarrBlock::AlbumHistoryDetails,
ActiveLidarrBlock::ManualAlbumSearch,
ActiveLidarrBlock::ManualAlbumSearchSortPrompt,
ActiveLidarrBlock::DeleteTrackFilePrompt
)]
active_sonarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
ArtistDetailsHandler,
ActiveLidarrBlock::Artists,
active_sonarr_block
);
}
#[test]
fn test_releases_sorting_options_source() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering =
@@ -15,7 +15,7 @@ mod tests {
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
LIBRARY_BLOCKS,
LIBRARY_BLOCKS, TRACK_DETAILS_BLOCKS,
};
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::network::lidarr_network::LidarrEvent;
@@ -34,10 +34,14 @@ mod tests {
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
library_handler_blocks.extend(ADD_ARTIST_BLOCKS);
library_handler_blocks.extend(ALBUM_DETAILS_BLOCKS);
library_handler_blocks.extend(TRACK_DETAILS_BLOCKS);
ActiveLidarrBlock::iter().for_each(|lidarr_block| {
if library_handler_blocks.contains(&lidarr_block) {
assert!(LibraryHandler::accepts(lidarr_block));
assert!(
LibraryHandler::accepts(lidarr_block),
"{lidarr_block} is not accepted by the LibraryHandler"
);
} else {
assert!(!LibraryHandler::accepts(lidarr_block));
}
@@ -670,6 +674,27 @@ mod tests {
);
}
#[rstest]
fn test_delegates_track_details_blocks_to_track_details_handler(
#[values(
ActiveLidarrBlock::TrackDetails,
ActiveLidarrBlock::TrackHistory,
ActiveLidarrBlock::TrackHistoryDetails,
ActiveLidarrBlock::SearchTrackHistory,
ActiveLidarrBlock::SearchTrackHistoryError,
ActiveLidarrBlock::FilterTrackHistory,
ActiveLidarrBlock::FilterTrackHistoryError,
ActiveLidarrBlock::TrackHistorySortPrompt
)]
active_sonarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LibraryHandler,
ActiveLidarrBlock::AlbumDetails,
active_sonarr_block
);
}
#[test]
fn test_edit_key() {
let mut app = App::test_default();
+20 -1
View File
@@ -19,18 +19,22 @@ use super::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
mod add_artist_handler;
mod album_details_handler;
mod artist_details_handler;
mod delete_album_handler;
mod delete_artist_handler;
mod edit_artist_handler;
mod track_details_handler;
use crate::handlers::lidarr_handlers::library::album_details_handler::AlbumDetailsHandler;
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
use crate::handlers::lidarr_handlers::library::track_details_handler::TrackDetailsHandler;
use crate::models::Route;
pub(in crate::handlers::lidarr_handlers) use add_artist_handler::AddArtistHandler;
pub(in crate::handlers::lidarr_handlers) use artist_details_handler::ArtistDetailsHandler;
pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler;
pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler;
mod album_details_handler;
#[cfg(test)]
#[path = "library_handler_tests.rs"]
mod library_handler_tests;
@@ -82,6 +86,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
ArtistDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => {
DeleteAlbumHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if AlbumDetailsHandler::accepts(self.active_lidarr_block) => {
AlbumDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if TrackDetailsHandler::accepts(self.active_lidarr_block) => {
TrackDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ => self.handle_key_event(),
}
}
@@ -90,8 +106,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
fn accepts(active_block: ActiveLidarrBlock) -> bool {
AddArtistHandler::accepts(active_block)
|| DeleteArtistHandler::accepts(active_block)
|| DeleteAlbumHandler::accepts(active_block)
|| EditArtistHandler::accepts(active_block)
|| ArtistDetailsHandler::accepts(active_block)
|| AlbumDetailsHandler::accepts(active_block)
|| TrackDetailsHandler::accepts(active_block)
|| LIBRARY_BLOCKS.contains(&active_block)
}
@@ -0,0 +1,225 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::history::history_sorting_options;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::matches_key;
use crate::models::Route;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
#[cfg(test)]
#[path = "track_details_handler_tests.rs"]
mod track_details_handler_tests;
pub(super) struct TrackDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for TrackDetailsHandler<'a, 'b> {
fn handle(&mut self) {
let track_history_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::TrackHistory.into())
.sorting_block(ActiveLidarrBlock::TrackHistorySortPrompt.into())
.sort_options(history_sorting_options())
.searching_block(ActiveLidarrBlock::SearchTrackHistory.into())
.search_error_block(ActiveLidarrBlock::SearchTrackHistoryError.into())
.search_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text)
.filtering_block(ActiveLidarrBlock::FilterTrackHistory.into())
.filter_error_block(ActiveLidarrBlock::FilterTrackHistoryError.into())
.filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text);
if !handle_table(
self,
|app| {
&mut app
.data
.lidarr_data
.album_details_modal
.as_mut()
.expect("Album details modal is undefined")
.track_details_modal
.as_mut()
.expect("Track details modal is undefined")
.track_history
},
track_history_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
TRACK_DETAILS_BLOCKS.contains(&active_block)
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
) -> Self {
Self {
key,
app,
active_lidarr_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
if self.app.is_loading {
return false;
}
let Some(album_details_modal) = self.app.data.lidarr_data.album_details_modal.as_ref() else {
return false;
};
let Some(track_details_modal) = &album_details_modal.track_details_modal else {
return false;
};
match self.active_lidarr_block {
ActiveLidarrBlock::TrackDetails => !track_details_modal.track_details.is_empty(),
ActiveLidarrBlock::TrackHistory => !track_details_modal.track_history.is_empty(),
_ => true,
}
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => match self.key {
_ if matches_key!(left, self.key) => {
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details_tabs
.previous();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
);
}
_ if matches_key!(right, self.key) => {
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details_tabs
.next();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
);
}
_ => (),
},
_ => (),
}
}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::TrackHistory {
self
.app
.push_navigation_stack(ActiveLidarrBlock::TrackHistoryDetails.into());
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => {
self.app.pop_navigation_stack();
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal = None;
}
ActiveLidarrBlock::TrackHistoryDetails => {
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => match self.key {
_ if matches_key!(refresh, self.key) => {
self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
_ => (),
},
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,407 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::track_details_handler::TrackDetailsHandler;
use crate::models::ScrollableText;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
mod test_handle_left_right_actions {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
#[case(ActiveLidarrBlock::TrackHistory, ActiveLidarrBlock::TrackDetails)]
fn test_track_details_tabs_left_right_action(
#[case] left_block: ActiveLidarrBlock,
#[case] right_block: ActiveLidarrBlock,
#[values(true, false)] is_ready: bool,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
app.is_loading = is_ready;
app.push_navigation_stack(right_block.into());
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details_tabs
.index = app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.tabs
.iter()
.position(|tab_route| tab_route.route == right_block.into())
.unwrap_or_default();
TrackDetailsHandler::new(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle();
assert_eq!(
app.get_current_route(),
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route()
);
assert_navigation_pushed!(app, left_block.into());
TrackDetailsHandler::new(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle();
assert_eq!(
app.get_current_route(),
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route()
);
assert_navigation_pushed!(app, right_block.into());
}
}
mod test_handle_submit {
use super::*;
use crate::event::Key;
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_eq;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_track_history_submit() {
let mut app = App::test_default_fully_populated();
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::TrackHistoryDetails.into());
}
#[test]
fn test_track_history_submit_no_op_when_track_history_is_empty() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_history = StatefulTable::default();
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::TrackHistory.into()
);
}
#[test]
fn test_track_history_submit_no_op_when_not_ready() {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::TrackHistory.into()
);
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
use crate::event::Key;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_track_history_details_block_esc() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
app.push_navigation_stack(ActiveLidarrBlock::TrackHistoryDetails.into());
TrackDetailsHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::TrackHistoryDetails,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::TrackHistory.into());
}
#[rstest]
fn test_track_details_tabs_esc(
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
app.push_navigation_stack(active_lidarr_block.into());
TrackDetailsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::AlbumDetails.into());
assert_none!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
}
}
mod test_handle_key_char {
use super::*;
use pretty_assertions::assert_eq;
#[rstest]
fn test_refresh_key(
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.is_routing = false;
TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_navigation_pushed!(app, active_lidarr_block.into());
assert!(app.is_routing);
}
#[rstest]
fn test_refresh_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(active_lidarr_block.into());
app.is_routing = false;
TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_lidarr_block.into());
assert!(!app.is_routing);
}
}
#[test]
fn test_track_details_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) {
assert!(TrackDetailsHandler::accepts(active_lidarr_block));
} else {
assert!(!TrackDetailsHandler::accepts(active_lidarr_block));
}
});
}
#[rstest]
fn test_track_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_track_details_handler_is_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
app.is_loading = true;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_track_details_handler_is_not_ready_when_album_details_modal_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_track_details_handler_is_not_ready_when_track_details_modal_is_empty() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal = None;
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_track_details_handler_is_not_ready_when_track_details_is_empty() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details = ScrollableText::default();
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_track_details_handler_is_not_ready_when_track_history_table_is_empty() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_history = StatefulTable::default();
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackHistory,
None,
);
assert!(!handler.is_ready());
}
#[rstest]
fn test_track_details_handler_is_ready(
#[values(
ActiveLidarrBlock::TrackDetails,
ActiveLidarrBlock::TrackHistory,
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
);
assert!(handler.is_ready());
}
}
+3
View File
@@ -422,6 +422,8 @@ pub struct LidarrHistoryItem {
pub album_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub artist_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub track_id: i64,
#[serde(default)]
pub quality: QualityWrapper,
pub date: DateTime<Utc>,
@@ -556,6 +558,7 @@ pub struct Track {
pub duration: i64,
pub has_file: bool,
pub ratings: Ratings,
pub track_file: Option<TrackFile>,
}
impl From<LidarrSerdeable> for Serdeable {
+35 -1
View File
@@ -27,6 +27,7 @@ use itertools::Itertools;
use strum::EnumIter;
#[cfg(test)]
use {
super::modals::TrackDetailsModal,
crate::models::lidarr_models::{MonitorType, NewItemMonitorType},
crate::models::stateful_table::SortOption,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings,
@@ -292,7 +293,21 @@ impl LidarrData<'_> {
.metadata_profile_list
.set_items(vec![metadata_profile().name]);
let mut album_details_modal = AlbumDetailsModal::default();
let mut track_details_modal = TrackDetailsModal::default();
track_details_modal.track_details = ScrollableText::with_string("Some details".to_owned());
track_details_modal
.track_history
.set_items(vec![lidarr_history_item()]);
track_details_modal.track_history.search = Some("track history search".into());
track_details_modal.track_history.filter = Some("track history filter".into());
track_details_modal
.track_history
.sorting(vec![sort_option!(id)]);
let mut album_details_modal = AlbumDetailsModal {
track_details_modal: Some(track_details_modal),
..AlbumDetailsModal::default()
};
album_details_modal.tracks.set_items(vec![track()]);
album_details_modal.tracks.search = Some("album search".into());
album_details_modal
@@ -469,6 +484,8 @@ pub enum ActiveLidarrBlock {
FilterHistoryError,
FilterArtistHistory,
FilterArtistHistoryError,
FilterTrackHistory,
FilterTrackHistoryError,
History,
HistoryItemDetails,
HistorySortPrompt,
@@ -499,12 +516,18 @@ pub enum ActiveLidarrBlock {
SearchArtistHistoryError,
SearchTracks,
SearchTracksError,
SearchTrackHistory,
SearchTrackHistoryError,
System,
SystemLogs,
SystemQueuedEvents,
SystemTasks,
SystemTaskStartConfirmPrompt,
SystemUpdates,
TrackDetails,
TrackHistory,
TrackHistoryDetails,
TrackHistorySortPrompt,
UpdateAllArtistsPrompt,
UpdateAndScanArtistPrompt,
UpdateDownloadsPrompt,
@@ -767,6 +790,17 @@ pub static SYSTEM_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
ActiveLidarrBlock::SystemUpdates,
];
pub static TRACK_DETAILS_BLOCKS: [ActiveLidarrBlock; 8] = [
ActiveLidarrBlock::TrackDetails,
ActiveLidarrBlock::TrackHistory,
ActiveLidarrBlock::TrackHistoryDetails,
ActiveLidarrBlock::SearchTrackHistory,
ActiveLidarrBlock::SearchTrackHistoryError,
ActiveLidarrBlock::FilterTrackHistory,
ActiveLidarrBlock::FilterTrackHistoryError,
ActiveLidarrBlock::TrackHistorySortPrompt,
];
impl From<ActiveLidarrBlock> for Route {
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
Route::Lidarr(active_lidarr_block, None)
@@ -16,6 +16,7 @@ mod tests {
EDIT_ARTIST_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
TRACK_DETAILS_BLOCKS,
};
use crate::models::{
BlockSelectionState, Route,
@@ -695,4 +696,17 @@ mod tests {
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTaskStartConfirmPrompt));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemUpdates));
}
#[test]
fn test_track_details_blocks_contents() {
assert_eq!(TRACK_DETAILS_BLOCKS.len(), 8);
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackDetails));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistory));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistoryDetails));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTrackHistory));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTrackHistoryError));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterTrackHistory));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterTrackHistoryError));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistorySortPrompt));
}
}
+34 -3
View File
@@ -1,13 +1,14 @@
use super::lidarr_data::{ActiveLidarrBlock, LidarrData};
use crate::app::lidarr::lidarr_context_clues::{
ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
};
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track, TrackFile};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::Indexer;
use crate::models::stateful_table::StatefulTable;
use crate::models::{
HorizontallyScrollableText, TabRoute, TabState,
HorizontallyScrollableText, ScrollableText, TabRoute, TabState,
lidarr_models::{MonitorType, NewItemMonitorType},
servarr_models::RootFolder,
stateful_list::StatefulList,
@@ -226,7 +227,7 @@ impl From<&LidarrData<'_>> for AddRootFolderModal {
pub struct AlbumDetailsModal {
pub tracks: StatefulTable<Track>,
pub track_files: StatefulTable<TrackFile>,
// pub track_details_modal: Option<EpisodeDetailsModal>,
pub track_details_modal: Option<TrackDetailsModal>,
pub album_history: StatefulTable<LidarrHistoryItem>,
pub album_releases: StatefulTable<LidarrRelease>,
pub album_details_tabs: TabState,
@@ -236,7 +237,7 @@ impl Default for AlbumDetailsModal {
fn default() -> AlbumDetailsModal {
AlbumDetailsModal {
tracks: StatefulTable::default(),
// TODO episode_details_modal: None,
track_details_modal: None,
track_files: StatefulTable::default(),
album_releases: StatefulTable::default(),
album_history: StatefulTable::default(),
@@ -263,3 +264,33 @@ impl Default for AlbumDetailsModal {
}
}
}
#[cfg_attr(test, derive(Debug))]
pub struct TrackDetailsModal {
pub track_details: ScrollableText,
pub track_history: StatefulTable<LidarrHistoryItem>,
pub track_details_tabs: TabState,
}
impl Default for TrackDetailsModal {
fn default() -> Self {
TrackDetailsModal {
track_details: ScrollableText::default(),
track_history: StatefulTable::default(),
track_details_tabs: TabState::new(vec![
TabRoute {
title: "Track Details".to_string(),
route: ActiveLidarrBlock::TrackDetails.into(),
contextual_help: Some(&TRACK_DETAILS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveLidarrBlock::TrackHistory.into(),
contextual_help: Some(&TRACK_HISTORY_CONTEXT_CLUES),
config: None,
},
]),
}
}
}
+57 -39
View File
@@ -2,11 +2,12 @@
mod tests {
use crate::app::lidarr::lidarr_context_clues::{
ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
};
use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
use crate::models::servarr_data::lidarr::modals::{
AddArtistModal, AlbumDetailsModal, EditArtistModal,
AddArtistModal, AlbumDetailsModal, EditArtistModal, TrackDetailsModal,
};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
@@ -92,14 +93,14 @@ mod tests {
quality_profile_id: 1,
metadata_profile_id: 1,
path: "/nfs/music/test_artist".to_owned(),
tags: vec![serde_json::Number::from(1)],
tags: vec![Number::from(1)],
..Artist::default()
};
lidarr_data.artists.set_items(vec![artist]);
let edit_artist_modal = EditArtistModal::from(&lidarr_data);
assert_eq!(edit_artist_modal.monitored, Some(true));
assert_some_eq_x!(&edit_artist_modal.monitored, &true);
assert_eq!(
*edit_artist_modal.monitor_list.current_selection(),
NewItemMonitorType::All
@@ -205,24 +206,24 @@ mod tests {
let edit_indexer_modal = EditIndexerModal::from(&lidarr_data);
assert_str_eq!(edit_indexer_modal.name.text, "Test");
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
assert_some_eq_x!(&edit_indexer_modal.enable_rss, &true);
assert_some_eq_x!(&edit_indexer_modal.enable_automatic_search, &true);
assert_some_eq_x!(&edit_indexer_modal.enable_interactive_search, &true);
assert_eq!(edit_indexer_modal.priority, 1);
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
assert_is_empty!(edit_indexer_modal.seed_ratio.text);
}
#[test]
fn test_album_details_modal_default() {
let album_details_modal = AlbumDetailsModal::default();
assert!(album_details_modal.tracks.is_empty());
// assert!(album_details_modal.track_details_modal.is_none());
assert!(album_details_modal.track_files.is_empty());
assert!(album_details_modal.album_releases.is_empty());
assert!(album_details_modal.album_history.is_empty());
assert_is_empty!(album_details_modal.tracks);
assert_none!(album_details_modal.track_details_modal);
assert_is_empty!(album_details_modal.track_files);
assert_is_empty!(album_details_modal.album_releases);
assert_is_empty!(album_details_modal.album_history);
assert_eq!(album_details_modal.album_details_tabs.tabs.len(), 3);
@@ -234,15 +235,8 @@ mod tests {
album_details_modal.album_details_tabs.tabs[0].route,
ActiveLidarrBlock::AlbumDetails.into()
);
assert!(
album_details_modal.album_details_tabs.tabs[0]
.contextual_help
.is_some()
);
assert_eq!(
album_details_modal.album_details_tabs.tabs[0]
.contextual_help
.unwrap(),
assert_some_eq_x!(
&album_details_modal.album_details_tabs.tabs[0].contextual_help,
&ALBUM_DETAILS_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[0].config, None);
@@ -255,15 +249,8 @@ mod tests {
album_details_modal.album_details_tabs.tabs[1].route,
ActiveLidarrBlock::AlbumHistory.into()
);
assert!(
album_details_modal.album_details_tabs.tabs[1]
.contextual_help
.is_some()
);
assert_eq!(
album_details_modal.album_details_tabs.tabs[1]
.contextual_help
.unwrap(),
assert_some_eq_x!(
&album_details_modal.album_details_tabs.tabs[1].contextual_help,
&ALBUM_HISTORY_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[1].config, None);
@@ -276,17 +263,48 @@ mod tests {
album_details_modal.album_details_tabs.tabs[2].route,
ActiveLidarrBlock::ManualAlbumSearch.into()
);
assert!(
album_details_modal.album_details_tabs.tabs[2]
.contextual_help
.is_some()
);
assert_eq!(
album_details_modal.album_details_tabs.tabs[2]
.contextual_help
.unwrap(),
assert_some_eq_x!(
&album_details_modal.album_details_tabs.tabs[2].contextual_help,
&MANUAL_ALBUM_SEARCH_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[2].config, None);
}
#[test]
fn test_track_details_modal_default() {
let track_details_modal = TrackDetailsModal::default();
assert_is_empty!(track_details_modal.track_details);
assert_is_empty!(track_details_modal.track_history);
assert_eq!(track_details_modal.track_details_tabs.tabs.len(), 2);
assert_str_eq!(
track_details_modal.track_details_tabs.tabs[0].title,
"Track Details"
);
assert_eq!(
track_details_modal.track_details_tabs.tabs[0].route,
ActiveLidarrBlock::TrackDetails.into()
);
assert_some_eq_x!(
&track_details_modal.track_details_tabs.tabs[0].contextual_help,
&TRACK_DETAILS_CONTEXT_CLUES
);
assert_eq!(track_details_modal.track_details_tabs.tabs[0].config, None);
assert_str_eq!(
track_details_modal.track_details_tabs.tabs[1].title,
"History"
);
assert_eq!(
track_details_modal.track_details_tabs.tabs[1].route,
ActiveLidarrBlock::TrackHistory.into()
);
assert_some_eq_x!(
&track_details_modal.track_details_tabs.tabs[1].contextual_help,
&TRACK_HISTORY_CONTEXT_CLUES
);
assert_eq!(track_details_modal.track_details_tabs.tabs[1].config, None);
}
}
@@ -17,6 +17,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -30,6 +31,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -49,6 +51,7 @@ mod tests {
id: 123,
album_id: 1007,
artist_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -56,6 +59,7 @@ mod tests {
id: 456,
album_id: 2001,
artist_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -113,6 +117,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -126,6 +131,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -3,6 +3,7 @@ mod tests {
use crate::models::lidarr_models::{
Album, DeleteParams, LidarrHistoryItem, LidarrRelease, LidarrSerdeable,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::stateful_table::SortOption;
use crate::network::lidarr_network::LidarrEvent;
@@ -146,6 +147,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -159,6 +161,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -173,6 +176,7 @@ mod tests {
id: 123,
artist_id: 1007,
album_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -180,6 +184,7 @@ mod tests {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -270,6 +275,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -283,6 +289,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -297,6 +304,7 @@ mod tests {
id: 123,
artist_id: 1007,
album_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -304,6 +312,7 @@ mod tests {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -350,6 +359,95 @@ mod tests {
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_album_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]);
let response: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=1&albumId=1")
.build_for(LidarrEvent::GetAlbumHistory(1, 1))
.await;
app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_history
.sort_asc = true;
app
.lock()
.await
.push_navigation_stack(ActiveLidarrBlock::AlbumHistorySortPrompt.into());
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetAlbumHistory(1, 1))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
assert_is_empty!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_history
.items
);
assert!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_history
.sort_asc
);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_album_releases_event() {
let release_json = json!([
@@ -1,7 +1,8 @@
use crate::models::Route;
use crate::models::lidarr_models::{
Album, DeleteParams, LidarrCommandBody, LidarrHistoryItem, LidarrRelease,
};
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
@@ -75,28 +76,25 @@ impl Network<'_, '_> {
self
.handle_request::<(), Vec<LidarrHistoryItem>>(request_props, |history_items, mut app| {
if app.data.lidarr_data.album_details_modal.is_none() {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
let is_sorting = matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::AlbumHistorySortPrompt, _)
);
let mut history_vec = history_items;
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_history
.set_items(history_vec);
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_history
.apply_sorting_toggle(false);
if !is_sorting {
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.get_or_insert_default();
let mut history_vec = history_items;
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
album_details_modal.album_history.set_items(history_vec);
album_details_modal
.album_history
.apply_sorting_toggle(false);
}
})
.await
}
@@ -121,21 +119,18 @@ impl Network<'_, '_> {
self
.handle_request::<(), Vec<LidarrRelease>>(request_props, |release_vec, mut app| {
if app.data.lidarr_data.album_details_modal.is_none() {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.get_or_insert_default();
let album_releases_vec = release_vec
.into_iter()
.filter(|release| !release.discography)
.collect();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
album_details_modal
.album_releases
.set_items(album_releases_vec);
})
@@ -113,6 +113,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -126,6 +127,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -140,6 +142,7 @@ mod tests {
id: 123,
artist_id: 1007,
album_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -147,6 +150,7 @@ mod tests {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -212,6 +216,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -225,6 +230,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -239,6 +245,7 @@ mod tests {
id: 123,
artist_id: 1007,
album_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -246,6 +253,7 @@ mod tests {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -289,6 +297,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -302,6 +311,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -1,11 +1,17 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable, Track, TrackFile};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::{AlbumDetailsModal, TrackDetailsModal};
use crate::models::stateful_table::SortOption;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{track, track_file};
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
lidarr_history_item, track, track_file,
};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use pretty_assertions::assert_eq;
use indoc::formatdoc;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::json;
#[tokio::test]
@@ -28,6 +34,272 @@ mod tests {
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_track_details_event() {
let response = track();
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(track()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
});
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let result = network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await;
async_server.assert_async().await;
assert_ok!(&result);
let LidarrSerdeable::Track(track) = result.unwrap() else {
panic!("Expected Track")
};
assert_eq!(track, response);
let app = app_arc.lock().await;
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
ActiveLidarrBlock::TrackDetails.into()
);
let track_details = &app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details;
assert_str_eq!(
track_details.get_text(),
formatdoc!(
"
Title: Test title
Track Number: 1
Duration: 3:20
Explicit: false
Quality: Lossless
File Path: /music/P!nk/TRUSTFALL/01 - When I Get There.flac
File Size: 37.40 MB
Date Added: 2023-05-20 21:29:16 UTC
Codec: FLAC
Channels: 2
Bits: 24bit
Bit Rate: 1563 kbps
Sample Rate: 44.1kHz
"
)
)
}
#[tokio::test]
async fn test_handle_get_track_details_event_empty_media_info() {
let expected_track = Track {
track_file: Some(TrackFile {
media_info: None,
..track_file()
}),
..track()
};
let response = expected_track.clone();
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(expected_track.clone()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
});
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let result = network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await;
async_server.assert_async().await;
assert_ok!(&result);
let LidarrSerdeable::Track(track) = result.unwrap() else {
panic!("Expected Track")
};
assert_eq!(track, response);
let app = app_arc.lock().await;
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
ActiveLidarrBlock::TrackDetails.into()
);
let track_details = &app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details;
assert_str_eq!(
track_details.get_text(),
formatdoc!(
"
Title: Test title
Track Number: 1
Duration: 3:20
Explicit: false
Quality: Lossless
File Path: /music/P!nk/TRUSTFALL/01 - When I Get There.flac
File Size: 37.40 MB
Date Added: 2023-05-20 21:29:16 UTC
"
)
)
}
#[tokio::test]
async fn test_handle_get_track_details_event_empty_track_file() {
let expected_track = Track {
track_file: None,
..track()
};
let response = expected_track.clone();
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(expected_track.clone()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
});
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let result = network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await;
async_server.assert_async().await;
assert_ok!(&result);
let LidarrSerdeable::Track(track) = result.unwrap() else {
panic!("Expected Track")
};
assert_eq!(track, response);
let app = app_arc.lock().await;
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
ActiveLidarrBlock::TrackDetails.into()
);
let track_details = &app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details;
assert_str_eq!(
track_details.get_text(),
formatdoc!(
"
Title: Test title
Track Number: 1
Duration: 3:20
Explicit: false
"
)
)
}
#[tokio::test]
async fn test_handle_get_track_details_event_album_details_modal_not_required_in_cli_mode() {
let response = track();
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(track()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.cli_mode = true;
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let result = network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await;
async_server.assert_async().await;
assert_ok!(&result);
let LidarrSerdeable::Track(track) = result.unwrap() else {
panic!("Expected Track")
};
assert_eq!(track, response);
let app = app_arc.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_some!(
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
}
#[tokio::test]
#[should_panic(expected = "Album details modal is empty")]
async fn test_handle_get_track_details_event_requires_album_details_modal_to_be_some_when_in_tui_mode()
{
let (_async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(track()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await
.unwrap();
}
#[tokio::test]
async fn test_handle_get_tracks_event() {
let expected_tracks = vec![track()];
@@ -170,4 +442,430 @@ mod tests {
);
assert_eq!(track_files, vec![track_file()]);
}
#[rstest]
#[tokio::test]
async fn test_handle_get_lidarr_track_history_event(
#[values(true, false)] use_custom_sorting: bool,
) {
let history_json = json!([{
"id": 123,
"sourceTitle": "z track",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "A Track",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]);
let response: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let mut expected_history_items = vec![LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "A Track".into(),
..lidarr_history_item()
}];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=2001&albumId=2001")
.build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await;
let album_details_modal = AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
};
app.lock().await.data.lidarr_data.album_details_modal = Some(album_details_modal);
if use_custom_sorting {
let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
expected_history_items.sort_by(cmp_fn);
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_history
.sorting(vec![history_sort_option]);
}
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_history
.sort_asc = true;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.items,
expected_history_items
);
assert!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.sort_asc
);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_track_history_event_empty_track_details_modal() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z track",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "A Track",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]);
let response: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let expected_history_items = vec![LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "A Track".into(),
..lidarr_history_item()
}];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=2001&albumId=2001")
.build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await;
app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_some!(
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.items,
expected_history_items
);
assert!(
!app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.sort_asc
);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_track_history_event_empty_album_details_modal() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z track",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "A Track",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]);
let response: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let expected_history_items = vec![LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "A Track".into(),
..lidarr_history_item()
}];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=2001&albumId=2001")
.build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_some!(
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.items,
expected_history_items
);
assert!(
!app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.sort_asc
);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_track_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z track",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "A Track",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]);
let response: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=2001&albumId=2001")
.build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await;
let album_details_modal = AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
};
app.lock().await.data.lidarr_data.album_details_modal = Some(album_details_modal);
app.lock().await.server_tabs.set_index(2);
app
.lock()
.await
.push_navigation_stack(ActiveLidarrBlock::TrackHistorySortPrompt.into());
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_some!(
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
assert_is_empty!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.items,
);
assert!(
!app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.sort_asc
);
assert_eq!(history, response);
}
}
+159 -15
View File
@@ -1,8 +1,11 @@
use crate::models::lidarr_models::{Track, TrackFile};
use crate::models::lidarr_models::{LidarrHistoryItem, MediaInfo, Track, TrackFile};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::{Route, ScrollableText};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use indoc::formatdoc;
use log::info;
#[cfg(test)]
@@ -53,18 +56,117 @@ impl Network<'_, '_> {
self
.handle_request::<(), Vec<Track>>(request_props, |mut track_vec, mut app| {
track_vec.sort_by(|a, b| a.id.cmp(&b.id));
if app.data.lidarr_data.album_details_modal.is_none() {
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.get_or_insert_default();
album_details_modal.tracks.set_items(track_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_track_details(
&mut self,
track_id: i64,
) -> Result<Track> {
let event = LidarrEvent::GetTrackDetails(track_id);
info!("Fetching Lidarr track details for track with ID: {track_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
Some(format!("/{track_id}")),
None,
)
.await;
self
.handle_request::<(), Track>(request_props, |track_response, mut app| {
if app.cli_mode {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
app
let Track {
explicit,
track_number,
title,
duration,
track_file,
..
} = track_response;
let duration_secs = duration / 1000;
let mins = duration_secs / 60;
let secs = duration_secs % 60;
let track_length = format!("{mins}:{secs:02}");
let track_details_modal = app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.tracks
.set_items(track_vec);
.expect("Album details modal is empty")
.track_details_modal
.get_or_insert_default();
let mut details = formatdoc!(
"
Title: {title}
Track Number: {track_number}
Duration: {track_length}
Explicit: {explicit}
"
);
if let Some(file) = track_file {
let TrackFile {
path,
size,
quality,
date_added,
media_info,
..
} = file;
let quality_name = quality.quality.name;
let size_mb = size as f64 / 1024f64.powi(2);
details.push_str(&formatdoc!(
"
Quality: {quality_name}
File Path: {path}
File Size: {size_mb:.2} MB
Date Added: {date_added}
"
));
if let Some(info) = media_info {
let MediaInfo {
audio_bit_rate,
audio_channels,
audio_codec,
audio_bits,
audio_sample_rate,
} = info;
details.push_str(&formatdoc!(
"
Codec: {}
Channels: {}
Bits: {}
Bit Rate: {}
Sample Rate: {}
",
audio_codec.unwrap_or_default(),
audio_channels,
audio_bits.unwrap_or_default(),
audio_bit_rate.unwrap_or_default(),
audio_sample_rate.unwrap_or_default()
));
}
}
track_details_modal.track_details = ScrollableText::with_string(details);
})
.await
}
@@ -88,18 +190,60 @@ impl Network<'_, '_> {
self
.handle_request::<(), Vec<TrackFile>>(request_props, |track_file_vec, mut app| {
if app.data.lidarr_data.album_details_modal.is_none() {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
app
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_files
.set_items(track_file_vec);
.get_or_insert_default();
album_details_modal.track_files.set_items(track_file_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_track_history(
&mut self,
artist_id: i64,
album_id: i64,
track_id: i64,
) -> Result<Vec<LidarrHistoryItem>> {
let event = LidarrEvent::GetTrackHistory(artist_id, album_id, track_id);
info!(
"Fetching history for artist with ID: {artist_id} and album with ID: {album_id} and track with ID: {track_id}"
);
let params = format!("artistId={artist_id}&albumId={album_id}");
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
.await;
self
.handle_request::<(), Vec<LidarrHistoryItem>>(request_props, |history_items, mut app| {
let is_sorting = matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::TrackHistorySortPrompt, _)
);
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.get_or_insert_default();
let track_details_modal = album_details_modal
.track_details_modal
.get_or_insert_default();
if !is_sorting {
let mut history_vec: Vec<LidarrHistoryItem> = history_items
.into_iter()
.filter(|it| it.track_id == track_id)
.collect();
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
track_details_modal.track_history.set_items(history_vec);
track_details_modal
.track_history
.apply_sorting_toggle(false);
}
})
.await
}
@@ -278,6 +278,7 @@ pub mod test_utils {
source_title: "Test source title".into(),
album_id: 1,
artist_id: 1,
track_id: 1,
quality: quality_wrapper(),
date: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
event_type: LidarrHistoryEventType::Grabbed,
@@ -473,6 +474,7 @@ pub mod test_utils {
duration: 200173,
has_file: false,
ratings: ratings(),
track_file: Some(track_file()),
}
}
}
@@ -62,7 +62,11 @@ mod tests {
#[rstest]
fn test_resource_artist_history(
#[values(LidarrEvent::GetArtistHistory(0), LidarrEvent::GetAlbumHistory(0, 0))]
#[values(
LidarrEvent::GetArtistHistory(0),
LidarrEvent::GetAlbumHistory(0, 0),
LidarrEvent::GetTrackHistory(0, 0, 0)
)]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/history/artist");
@@ -147,6 +151,13 @@ mod tests {
assert_str_eq!(event.resource(), "/trackfile");
}
#[rstest]
fn test_resource_track(
#[values(LidarrEvent::GetTracks(0, 0), LidarrEvent::GetTrackDetails(0))] event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/track");
}
#[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
#[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")]
@@ -161,7 +172,6 @@ mod tests {
#[case(LidarrEvent::GetHistory(0), "/history")]
#[case(LidarrEvent::TestIndexer(0), "/indexer/test")]
#[case(LidarrEvent::TestAllIndexers, "/indexer/testall")]
#[case(LidarrEvent::GetTracks(0, 0), "/track")]
fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) {
assert_str_eq!(event.resource(), expected_uri);
}
+14 -2
View File
@@ -61,8 +61,10 @@ pub enum LidarrEvent {
GetRootFolders,
GetSecurityConfig,
GetStatus,
GetTrackDetails(i64),
GetTracks(i64, i64),
GetTrackFiles(i64),
GetTrackHistory(i64, i64, i64),
GetUpdates,
GetTags,
GetTasks,
@@ -99,7 +101,9 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::ToggleAlbumMonitoring(_)
| LidarrEvent::GetAlbumDetails(_)
| LidarrEvent::DeleteAlbum(_) => "/album",
LidarrEvent::GetArtistHistory(_) | LidarrEvent::GetAlbumHistory(_, _) => "/history/artist",
LidarrEvent::GetArtistHistory(_)
| LidarrEvent::GetAlbumHistory(_, _)
| LidarrEvent::GetTrackHistory(_, _, _) => "/history/artist",
LidarrEvent::GetLogs(_) => "/log",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue",
@@ -128,7 +132,7 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::TestAllIndexers => "/indexer/testall",
LidarrEvent::GetStatus => "/system/status",
LidarrEvent::GetTasks => "/system/task",
LidarrEvent::GetTracks(_, _) => "/track",
LidarrEvent::GetTracks(_, _) | LidarrEvent::GetTrackDetails(_) => "/track",
LidarrEvent::GetUpdates => "/update",
LidarrEvent::HealthCheck => "/health",
LidarrEvent::SearchNewArtist(_) => "/artist/lookup",
@@ -267,6 +271,10 @@ impl Network<'_, '_> {
LidarrEvent::GetStatus => self.get_lidarr_status().await.map(LidarrSerdeable::from),
LidarrEvent::GetTags => self.get_lidarr_tags().await.map(LidarrSerdeable::from),
LidarrEvent::GetTasks => self.get_lidarr_tasks().await.map(LidarrSerdeable::from),
LidarrEvent::GetTrackDetails(track_id) => self
.get_track_details(track_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetTracks(artist_id, album_id) => self
.get_tracks(artist_id, album_id)
.await
@@ -275,6 +283,10 @@ impl Network<'_, '_> {
.get_track_files(album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetTrackHistory(artist_id, album_id, track_id) => self
.get_lidarr_track_history(artist_id, album_id, track_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetUpdates => self.get_lidarr_updates().await.map(LidarrSerdeable::from),
LidarrEvent::HealthCheck => self
.get_lidarr_healthcheck()
@@ -1,31 +0,0 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Indexer RSS Automatic Search Interactive Search Priority Tags
=> Test Indexer Enabled Enabled Enabled 25 alex
╭────────────── Success ──────────────╮
│ Indexer test succeeded! │
│ │
╰───────────────────────────────────────╯
+7 -1
View File
@@ -2,6 +2,7 @@ use crate::app::App;
use crate::models::Route;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track};
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi;
use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details;
use crate::ui::styles::{ManagarrStyle, secondary_style};
use crate::ui::utils::{
@@ -31,10 +32,11 @@ impl DrawUi for AlbumDetailsUi {
let Route::Lidarr(active_lidarr_block, _) = route else {
return false;
};
ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block)
TrackDetailsUi::accepts(route) || ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
let route = app.get_current_route();
if app.data.lidarr_data.album_details_modal.is_some()
&& let Route::Lidarr(active_lidarr_block, _) = app.get_current_route()
{
@@ -106,6 +108,10 @@ impl DrawUi for AlbumDetailsUi {
};
draw_popup(f, app, draw_album_details_popup, Size::XLarge);
if TrackDetailsUi::accepts(route) {
TrackDetailsUi::draw(f, app, _area);
}
}
}
}
@@ -3,7 +3,9 @@ mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
use crate::models::servarr_data::lidarr::lidarr_data::{
ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock, TRACK_DETAILS_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::album_details_ui::AlbumDetailsUi;
@@ -11,8 +13,11 @@ mod tests {
#[test]
fn test_album_details_ui_accepts() {
let mut album_details_blocks = ALBUM_DETAILS_BLOCKS.to_vec();
album_details_blocks.extend(TRACK_DETAILS_BLOCKS);
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) {
if album_details_blocks.contains(&active_lidarr_block) {
assert!(AlbumDetailsUi::accepts(active_lidarr_block.into()));
} else {
assert!(!AlbumDetailsUi::accepts(active_lidarr_block.into()));
@@ -127,5 +132,17 @@ mod tests {
output
);
}
#[test]
fn test_album_details_ui_renders_track_details_over_album_details() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AlbumDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
@@ -4,6 +4,7 @@ mod tests {
use crate::models::servarr_data::lidarr::lidarr_data::{
ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
TRACK_DETAILS_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi;
@@ -13,6 +14,7 @@ mod tests {
let mut blocks = ARTIST_DETAILS_BLOCKS.clone().to_vec();
blocks.extend(DELETE_ALBUM_BLOCKS);
blocks.extend(ALBUM_DETAILS_BLOCKS);
blocks.extend(TRACK_DETAILS_BLOCKS);
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if blocks.contains(&active_lidarr_block) {
+10 -2
View File
@@ -6,6 +6,7 @@ mod tests {
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS,
TRACK_DETAILS_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::{LibraryUi, decorate_artist_row_with_style};
@@ -23,12 +24,19 @@ mod tests {
library_ui_blocks.extend(ADD_ARTIST_BLOCKS);
library_ui_blocks.extend(ARTIST_DETAILS_BLOCKS);
library_ui_blocks.extend(ALBUM_DETAILS_BLOCKS);
library_ui_blocks.extend(TRACK_DETAILS_BLOCKS);
for active_lidarr_block in ActiveLidarrBlock::iter() {
if library_ui_blocks.contains(&active_lidarr_block) {
assert!(LibraryUi::accepts(active_lidarr_block.into()));
assert!(
LibraryUi::accepts(active_lidarr_block.into()),
"{active_lidarr_block} is not accepted by the LibraryUi"
);
} else {
assert!(!LibraryUi::accepts(active_lidarr_block.into()));
assert!(
!LibraryUi::accepts(active_lidarr_block.into()),
"{active_lidarr_block} should not be accepted by LibraryUi"
);
}
}
}
+2 -1
View File
@@ -31,10 +31,11 @@ use crate::{
mod add_artist_ui;
mod album_details_ui;
mod artist_details_ui;
mod delete_album_ui;
mod delete_artist_ui;
mod edit_artist_ui;
mod track_details_ui;
mod delete_album_ui;
#[cfg(test)]
#[path = "library_ui_tests.rs"]
mod library_ui_tests;
@@ -0,0 +1,50 @@
---
source: src/ui/lidarr_ui/library/album_details_ui_tests.rs
expression: output
---
╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Tracks │ History │ Manual Search │
│──────╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮─────│
│ # │ Track Details │ History │ │
│=> 1 │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │
│ │Some details: │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -1,48 +0,0 @@
---
source: src/ui/lidarr_ui/library/library_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex
╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭───╮ │
│ Monitored: │ ✔ │ │
│ ╰───╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor New Albums: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Quality Profile: │Lossless ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Metadata Profile: │Standard ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Tags: │alex │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ │
│ │
│ Loading ... │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ │
│ │
│ Loading ... │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭────────── Error ──────────╮ │
│ │ The given filter produced │ │
│ ╰─────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────── Filter ───────────╮ │
│ │track history filter │ │
│ ╰────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭────────── Error ──────────╮ │
│ │ No items found matching │ │
│ ╰─────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────── Search ───────────╮ │
│ │track history search │ │
│ ╰────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│Some details: │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │
│ │Source Title: Test source title │ │
│ │Event Type: grabbed │ │
│ │Quality: Lossless │ │
│ │Date: 2023-01-01 00:00:00 UTC │ │
│ │Indexer: │ │
│ │NZB Info URL: │ │
│ │Release Group: │ │
│ │Age: 0 days │ │
│ │Published Date: 1970-01-01 00:00:00 UTC │ │
│ │Download Client: │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭──────────────────────╮ │
│ │Something │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ╰──────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/library/track_details_ui_tests.rs
expression: output
---
╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Track Details │ History │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,259 @@
use crate::app::App;
use crate::models::Route;
use crate::models::lidarr_models::{LidarrHistoryItem, Track};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details;
use crate::ui::styles::ManagarrStyle;
use crate::ui::styles::{downloaded_style, missing_style, secondary_style};
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs};
use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
#[cfg(test)]
#[path = "track_details_ui_tests.rs"]
mod track_details_ui_tests;
pub(super) struct TrackDetailsUi;
impl DrawUi for TrackDetailsUi {
fn accepts(route: Route) -> bool {
let Route::Lidarr(active_lidarr_block, _) = route else {
return false;
};
TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
if let Some(album_details_modal) = app.data.lidarr_data.album_details_modal.as_ref()
&& album_details_modal.track_details_modal.is_some()
&& let Route::Lidarr(active_lidarr_block, _) = app.get_current_route()
{
let draw_track_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| {
let content_area = draw_tabs(
f,
popup_area,
"Track Details",
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("album_details_modal must exist in this context")
.track_details_modal
.as_ref()
.expect("track_details_modal must exist in this context")
.track_details_tabs,
);
draw_track_details_tabs(f, app, content_area);
if active_lidarr_block == ActiveLidarrBlock::TrackHistoryDetails {
draw_history_item_details_popup(f, app);
}
};
draw_popup(f, app, draw_track_details_popup, Size::Large);
}
}
}
pub fn draw_track_details_tabs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Some(album_details_modal) = app.data.lidarr_data.album_details_modal.as_ref()
&& let Some(track_details_modal) = album_details_modal.track_details_modal.as_ref()
&& let Route::Lidarr(active_lidarr_block, _) =
track_details_modal.track_details_tabs.get_active_route()
{
match active_lidarr_block {
ActiveLidarrBlock::TrackDetails => draw_track_details(f, app, area),
ActiveLidarrBlock::TrackHistory => draw_track_history_table(f, app, area),
_ => (),
}
}
}
fn draw_track_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let block = layout_block_top_border();
match app.data.lidarr_data.album_details_modal.as_ref() {
Some(album_details_modal) if !app.is_loading => {
if let Some(track_details_modal) = album_details_modal.track_details_modal.as_ref() {
let track = album_details_modal.tracks.current_selection().clone();
let track_details = &track_details_modal.track_details;
let text = Text::from(
track_details
.items
.iter()
.filter(|it| !it.is_empty())
.map(|line| {
let split = line.split(':').collect::<Vec<&str>>();
let title = format!("{}:", split[0]);
let style = style_from_status(&track);
Line::from(vec![
title.bold().style(style),
Span::styled(split[1..].join(":"), style),
])
})
.collect::<Vec<Line<'_>>>(),
);
let paragraph = Paragraph::new(text)
.block(block)
.wrap(Wrap { trim: false })
.scroll((track_details.offset, 0));
f.render_widget(paragraph, area);
}
}
_ => f.render_widget(
LoadingBlock::new(
app.is_loading
|| app
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("album_details_modal must exist in this context")
.track_details_modal
.is_none(),
block,
),
area,
),
}
}
fn draw_track_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match app.data.lidarr_data.album_details_modal.as_ref() {
Some(album_details_modal) if !app.is_loading => {
let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() else {
panic!("Non-Lidarr route is being used");
};
if let Some(track_details_modal) = album_details_modal.track_details_modal.as_ref() {
let current_selection = if track_details_modal.track_history.is_empty() {
LidarrHistoryItem::default()
} else {
track_details_modal
.track_history
.current_selection()
.clone()
};
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 track_history_table = &mut app
.data
.lidarr_data
.album_details_modal
.as_mut()
.expect("album_details_modal must exist in this context")
.track_details_modal
.as_mut()
.expect("track_details_modal must exist in this context")
.track_history;
let history_table = ManagarrTable::new(Some(&mut track_history_table), history_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_lidarr_block == ActiveLidarrBlock::TrackHistorySortPrompt)
.searching(active_lidarr_block == ActiveLidarrBlock::SearchTrackHistory)
.search_produced_empty_results(
active_lidarr_block == ActiveLidarrBlock::SearchTrackHistoryError,
)
.filtering(active_lidarr_block == ActiveLidarrBlock::FilterTrackHistory)
.filter_produced_empty_results(
active_lidarr_block == ActiveLidarrBlock::FilterTrackHistoryError,
)
.headers(["Source Title", "Event Type", "Quality", "Date"])
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(20),
Constraint::Percentage(15),
Constraint::Percentage(25),
]);
f.render_widget(history_table, area);
}
}
_ => f.render_widget(
LoadingBlock::new(
app.is_loading
|| app
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("album_details_modal must exist in this context")
.track_details_modal
.is_none(),
layout_block_top_border(),
),
area,
),
}
}
fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let current_selection =
if let Some(album_details_modal) = app.data.lidarr_data.album_details_modal.as_ref() {
if let Some(track_details_modal) = album_details_modal.track_details_modal.as_ref() {
if track_details_modal.track_history.is_empty() {
LidarrHistoryItem::default()
} else {
track_details_modal
.track_history
.current_selection()
.clone()
}
} else {
LidarrHistoryItem::default()
}
} else {
LidarrHistoryItem::default()
};
let line_vec = create_history_event_details(current_selection);
let text = Text::from(line_vec);
let message = Message::new(text)
.title("Details")
.style(secondary_style())
.alignment(Alignment::Left);
f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area());
}
fn style_from_status(track: &Track) -> Style {
if !track.has_file {
return missing_style();
}
downloaded_style()
}
@@ -0,0 +1,132 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_track_details_ui_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) {
assert!(TrackDetailsUi::accepts(active_lidarr_block.into()));
} else {
assert!(!TrackDetailsUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use rstest::rstest;
use super::*;
#[rstest]
#[case(ActiveLidarrBlock::TrackDetails, 0)]
#[case(ActiveLidarrBlock::TrackHistory, 1)]
#[case(ActiveLidarrBlock::TrackHistoryDetails, 1)]
#[case(ActiveLidarrBlock::SearchTrackHistory, 1)]
#[case(ActiveLidarrBlock::SearchTrackHistoryError, 1)]
#[case(ActiveLidarrBlock::FilterTrackHistory, 1)]
#[case(ActiveLidarrBlock::FilterTrackHistoryError, 1)]
#[case(ActiveLidarrBlock::TrackHistorySortPrompt, 1)]
#[case(ActiveLidarrBlock::TrackHistoryDetails, 1)]
fn test_track_details_ui_renders(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details_tabs
.set_index(index);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
TrackDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(
format!("track_details_{active_lidarr_block}_{index}"),
output
);
}
#[rstest]
#[case(ActiveLidarrBlock::TrackDetails, 0)]
#[case(ActiveLidarrBlock::TrackHistory, 1)]
fn test_track_details_ui_renders_loading(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
) {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(active_lidarr_block.into());
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details_tabs
.set_index(index);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
TrackDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(
format!("loading_track_details_{active_lidarr_block}_{index}"),
output
);
}
#[rstest]
#[case(ActiveLidarrBlock::TrackDetails, 0)]
#[case(ActiveLidarrBlock::TrackHistory, 1)]
fn test_track_details_ui_renders_empty(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
{
let track_details_modal = app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap();
track_details_modal.track_details_tabs.set_index(index);
track_details_modal.track_details = Default::default();
track_details_modal.track_history = Default::default();
}
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
TrackDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(
format!("empty_track_details_{active_lidarr_block}_{index}"),
output
);
}
}
}
@@ -425,6 +425,7 @@ mod tests {
source_title: "\nTest Album - Artist Name".into(),
album_id: 100,
artist_id: 10,
track_id: 1,
event_type,
quality: QualityWrapper {
quality: Quality {