diff --git a/src/app/lidarr/lidarr_context_clues.rs b/src/app/lidarr/lidarr_context_clues.rs index f980b6a..2b734f4 100644 --- a/src/app/lidarr/lidarr_context_clues.rs +++ b/src/app/lidarr/lidarr_context_clues.rs @@ -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 diff --git a/src/app/lidarr/lidarr_context_clues_tests.rs b/src/app/lidarr/lidarr_context_clues_tests.rs index 5a0f1e6..92ef2e6 100644 --- a/src/app/lidarr/lidarr_context_clues_tests.rs +++ b/src/app/lidarr/lidarr_context_clues_tests.rs @@ -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(); diff --git a/src/app/lidarr/lidarr_tests.rs b/src/app/lidarr/lidarr_tests.rs index a2fa691..d6517da 100644 --- a/src/app/lidarr/lidarr_tests.rs +++ b/src/app/lidarr/lidarr_tests.rs @@ -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::(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::(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(); diff --git a/src/app/lidarr/mod.rs b/src/app/lidarr/mod.rs index ad3a4ae..3dca060 100644 --- a/src/app/lidarr/mod.rs +++ b/src/app/lidarr/mod.rs @@ -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 } diff --git a/src/cli/lidarr/get_command_handler.rs b/src/cli/lidarr/get_command_handler.rs index a6335d0..8654724 100644 --- a/src/cli/lidarr/get_command_handler.rs +++ b/src/cli/lidarr/get_command_handler.rs @@ -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 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) diff --git a/src/cli/lidarr/get_command_handler_tests.rs b/src/cli/lidarr/get_command_handler_tests.rs index 35b8930..c9ea9d4 100644 --- a/src/cli/lidarr/get_command_handler_tests.rs +++ b/src/cli/lidarr/get_command_handler_tests.rs @@ -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::( + 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); + } } } diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs index 1e9ac6f..3790983 100644 --- a/src/cli/lidarr/list_command_handler.rs +++ b/src/cli/lidarr/list_command_handler.rs @@ -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 = 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, diff --git a/src/cli/lidarr/list_command_handler_tests.rs b/src/cli/lidarr/list_command_handler_tests.rs index 76944f7..48f92d3 100644 --- a/src/cli/lidarr/list_command_handler_tests.rs +++ b/src/cli/lidarr/list_command_handler_tests.rs @@ -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::( + 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; diff --git a/src/cli/lidarr/manual_search_command_handler_tests.rs b/src/cli/lidarr/manual_search_command_handler_tests.rs index bf89049..41c0948 100644 --- a/src/cli/lidarr/manual_search_command_handler_tests.rs +++ b/src/cli/lidarr/manual_search_command_handler_tests.rs @@ -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() + ); } } } diff --git a/src/handlers/lidarr_handlers/library/album_details_handler.rs b/src/handlers/lidarr_handlers/library/album_details_handler.rs index 2186f81..4e69596 100644 --- a/src/handlers/lidarr_handlers/library/album_details_handler.rs +++ b/src/handlers/lidarr_handlers/library/album_details_handler.rs @@ -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()), diff --git a/src/handlers/lidarr_handlers/library/album_details_handler_tests.rs b/src/handlers/lidarr_handlers/library/album_details_handler_tests.rs index c56b6db..843b522 100644 --- a/src/handlers/lidarr_handlers/library/album_details_handler_tests.rs +++ b/src/handlers/lidarr_handlers/library/album_details_handler_tests.rs @@ -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() { diff --git a/src/handlers/lidarr_handlers/library/artist_details_handler.rs b/src/handlers/lidarr_handlers/library/artist_details_handler.rs index 7efe05f..6911ff8 100644 --- a/src/handlers/lidarr_handlers/library/artist_details_handler.rs +++ b/src/handlers/lidarr_handlers/library/artist_details_handler.rs @@ -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, + _context: Option, } 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, + _context: Option, ) -> ArtistDetailsHandler<'a, 'b> { ArtistDetailsHandler { key, app, active_lidarr_block: active_block, - context, + _context, } } diff --git a/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs b/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs index ad08d7b..2e6e15e 100644 --- a/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs +++ b/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs @@ -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 = diff --git a/src/handlers/lidarr_handlers/library/library_handler_tests.rs b/src/handlers/lidarr_handlers/library/library_handler_tests.rs index 5d81f22..b28cb15 100644 --- a/src/handlers/lidarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/lidarr_handlers/library/library_handler_tests.rs @@ -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(); diff --git a/src/handlers/lidarr_handlers/library/mod.rs b/src/handlers/lidarr_handlers/library/mod.rs index dae1c06..82db4b2 100644 --- a/src/handlers/lidarr_handlers/library/mod.rs +++ b/src/handlers/lidarr_handlers/library/mod.rs @@ -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) } diff --git a/src/handlers/lidarr_handlers/library/track_details_handler.rs b/src/handlers/lidarr_handlers/library/track_details_handler.rs new file mode 100644 index 0000000..0c765d8 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/track_details_handler.rs @@ -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, +} + +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, + ) -> 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() + } +} diff --git a/src/handlers/lidarr_handlers/library/track_details_handler_tests.rs b/src/handlers/lidarr_handlers/library/track_details_handler_tests.rs new file mode 100644 index 0000000..9529db7 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/track_details_handler_tests.rs @@ -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()); + } +} diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index f6b9eeb..84569b9 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -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, @@ -556,6 +558,7 @@ pub struct Track { pub duration: i64, pub has_file: bool, pub ratings: Ratings, + pub track_file: Option, } impl From for Serdeable { diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs index 7a8d63a..ed5cd5e 100644 --- a/src/models/servarr_data/lidarr/lidarr_data.rs +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -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 for Route { fn from(active_lidarr_block: ActiveLidarrBlock) -> Route { Route::Lidarr(active_lidarr_block, None) diff --git a/src/models/servarr_data/lidarr/lidarr_data_tests.rs b/src/models/servarr_data/lidarr/lidarr_data_tests.rs index 8558282..8c0f90d 100644 --- a/src/models/servarr_data/lidarr/lidarr_data_tests.rs +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -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)); + } } diff --git a/src/models/servarr_data/lidarr/modals.rs b/src/models/servarr_data/lidarr/modals.rs index bb1a4f9..c969c47 100644 --- a/src/models/servarr_data/lidarr/modals.rs +++ b/src/models/servarr_data/lidarr/modals.rs @@ -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, pub track_files: StatefulTable, - // pub track_details_modal: Option, + pub track_details_modal: Option, pub album_history: StatefulTable, pub album_releases: StatefulTable, 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, + 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, + }, + ]), + } + } +} diff --git a/src/models/servarr_data/lidarr/modals_tests.rs b/src/models/servarr_data/lidarr/modals_tests.rs index dd92e40..4419326 100644 --- a/src/models/servarr_data/lidarr/modals_tests.rs +++ b/src/models/servarr_data/lidarr/modals_tests.rs @@ -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); + } } diff --git a/src/network/lidarr_network/history/lidarr_history_network_tests.rs b/src/network/lidarr_network/history/lidarr_history_network_tests.rs index b60d796..49e88b9 100644 --- a/src/network/lidarr_network/history/lidarr_history_network_tests.rs +++ b/src/network/lidarr_network/history/lidarr_history_network_tests.rs @@ -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", diff --git a/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs b/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs index 13e3ecc..5955ae6 100644 --- a/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs +++ b/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs @@ -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 = 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!([ diff --git a/src/network/lidarr_network/library/albums/mod.rs b/src/network/lidarr_network/library/albums/mod.rs index 6a5f202..d99f1c5 100644 --- a/src/network/lidarr_network/library/albums/mod.rs +++ b/src/network/lidarr_network/library/albums/mod.rs @@ -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>(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>(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); }) diff --git a/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs b/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs index 8ecb4b3..58e4d09 100644 --- a/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs +++ b/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs @@ -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", diff --git a/src/network/lidarr_network/library/tracks/lidarr_tracks_network_tests.rs b/src/network/lidarr_network/library/tracks/lidarr_tracks_network_tests.rs index 57ee399..7899095 100644 --- a/src/network/lidarr_network/library/tracks/lidarr_tracks_network_tests.rs +++ b/src/network/lidarr_network/library/tracks/lidarr_tracks_network_tests.rs @@ -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 = 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 = 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 = 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 = 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); + } } diff --git a/src/network/lidarr_network/library/tracks/mod.rs b/src/network/lidarr_network/library/tracks/mod.rs index 7026a7c..d24ddb5 100644 --- a/src/network/lidarr_network/library/tracks/mod.rs +++ b/src/network/lidarr_network/library/tracks/mod.rs @@ -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>(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 { + 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>(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> { + 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>(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 = 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 } diff --git a/src/network/lidarr_network/lidarr_network_test_utils.rs b/src/network/lidarr_network/lidarr_network_test_utils.rs index a86468a..b2852b5 100644 --- a/src/network/lidarr_network/lidarr_network_test_utils.rs +++ b/src/network/lidarr_network/lidarr_network_test_utils.rs @@ -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()), } } } diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index fa9a126..e439266 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -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); } diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index 12dce26..b667384 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -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() diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexerPrompt.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexerPrompt.snap deleted file mode 100644 index 8023961..0000000 --- a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexerPrompt.snap +++ /dev/null @@ -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! │ - │ │ - ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/album_details_ui.rs b/src/ui/lidarr_ui/library/album_details_ui.rs index 64ee919..58f6ee6 100644 --- a/src/ui/lidarr_ui/library/album_details_ui.rs +++ b/src/ui/lidarr_ui/library/album_details_ui.rs @@ -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); + } } } } diff --git a/src/ui/lidarr_ui/library/album_details_ui_tests.rs b/src/ui/lidarr_ui/library/album_details_ui_tests.rs index be66f59..874ba33 100644 --- a/src/ui/lidarr_ui/library/album_details_ui_tests.rs +++ b/src/ui/lidarr_ui/library/album_details_ui_tests.rs @@ -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); + } } } diff --git a/src/ui/lidarr_ui/library/artist_details_ui_tests.rs b/src/ui/lidarr_ui/library/artist_details_ui_tests.rs index f96e0a9..0ce22c5 100644 --- a/src/ui/lidarr_ui/library/artist_details_ui_tests.rs +++ b/src/ui/lidarr_ui/library/artist_details_ui_tests.rs @@ -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) { diff --git a/src/ui/lidarr_ui/library/library_ui_tests.rs b/src/ui/lidarr_ui/library/library_ui_tests.rs index 05751ee..c2505e5 100644 --- a/src/ui/lidarr_ui/library/library_ui_tests.rs +++ b/src/ui/lidarr_ui/library/library_ui_tests.rs @@ -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" + ); } } } diff --git a/src/ui/lidarr_ui/library/mod.rs b/src/ui/lidarr_ui/library/mod.rs index a26a0e4..449a4f9 100644 --- a/src/ui/lidarr_ui/library/mod.rs +++ b/src/ui/lidarr_ui/library/mod.rs @@ -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; diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_ui_renders_track_details_over_album_details.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_ui_renders_track_details_over_album_details.snap new file mode 100644 index 0000000..ec6133e --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_ui_renders_track_details_over_album_details.snap @@ -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: │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_edit_artist_over_artist_details.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_edit_artist_over_artist_details.snap deleted file mode 100644 index 8539203..0000000 --- a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_edit_artist_over_artist_details.snap +++ /dev/null @@ -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 ││ - │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ - ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackDetails_0.snap new file mode 100644 index 0000000..4a35aea --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackDetails_0.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackHistory_1.snap new file mode 100644 index 0000000..4a35aea --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackHistory_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackDetails_0.snap new file mode 100644 index 0000000..feb5ccc --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackDetails_0.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackHistory_1.snap new file mode 100644 index 0000000..feb5ccc --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackHistory_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistoryError_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistoryError_1.snap new file mode 100644 index 0000000..3d6a1c1 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistoryError_1.snap @@ -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 │ │ + │ ╰─────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistory_1.snap new file mode 100644 index 0000000..305bdb2 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistory_1.snap @@ -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 │ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistoryError_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistoryError_1.snap new file mode 100644 index 0000000..5e7404b --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistoryError_1.snap @@ -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 │ │ + │ ╰─────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistory_1.snap new file mode 100644 index 0000000..06e43e1 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistory_1.snap @@ -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 │ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackDetails_0.snap new file mode 100644 index 0000000..021e7df --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackDetails_0.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │Some details: │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistoryDetails_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistoryDetails_1.snap new file mode 100644 index 0000000..bffdaef --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistoryDetails_1.snap @@ -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: │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistorySortPrompt_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistorySortPrompt_1.snap new file mode 100644 index 0000000..9936c81 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistorySortPrompt_1.snap @@ -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 │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰──────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistory_1.snap new file mode 100644 index 0000000..902df42 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistory_1.snap @@ -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 │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/track_details_ui.rs b/src/ui/lidarr_ui/library/track_details_ui.rs new file mode 100644 index 0000000..cf330cc --- /dev/null +++ b/src/ui/lidarr_ui/library/track_details_ui.rs @@ -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::>(); + 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::>>(), + ); + + 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() +} diff --git a/src/ui/lidarr_ui/library/track_details_ui_tests.rs b/src/ui/lidarr_ui/library/track_details_ui_tests.rs new file mode 100644 index 0000000..cacbb4b --- /dev/null +++ b/src/ui/lidarr_ui/library/track_details_ui_tests.rs @@ -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 + ); + } + } +} diff --git a/src/ui/lidarr_ui/lidarr_ui_utils_tests.rs b/src/ui/lidarr_ui/lidarr_ui_utils_tests.rs index 40580c1..aad67b8 100644 --- a/src/ui/lidarr_ui/lidarr_ui_utils_tests.rs +++ b/src/ui/lidarr_ui/lidarr_ui_utils_tests.rs @@ -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 {