feat: CLI and TUI support for track history and track details in Lidarr

This commit is contained in:
2026-01-19 14:50:20 -07:00
parent 7add62b245
commit eff1a901eb
54 changed files with 3462 additions and 329 deletions
+3
View File
@@ -422,6 +422,8 @@ pub struct LidarrHistoryItem {
pub album_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub artist_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub track_id: i64,
#[serde(default)]
pub quality: QualityWrapper,
pub date: DateTime<Utc>,
@@ -556,6 +558,7 @@ pub struct Track {
pub duration: i64,
pub has_file: bool,
pub ratings: Ratings,
pub track_file: Option<TrackFile>,
}
impl From<LidarrSerdeable> for Serdeable {
+35 -1
View File
@@ -27,6 +27,7 @@ use itertools::Itertools;
use strum::EnumIter;
#[cfg(test)]
use {
super::modals::TrackDetailsModal,
crate::models::lidarr_models::{MonitorType, NewItemMonitorType},
crate::models::stateful_table::SortOption,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings,
@@ -292,7 +293,21 @@ impl LidarrData<'_> {
.metadata_profile_list
.set_items(vec![metadata_profile().name]);
let mut album_details_modal = AlbumDetailsModal::default();
let mut track_details_modal = TrackDetailsModal::default();
track_details_modal.track_details = ScrollableText::with_string("Some details".to_owned());
track_details_modal
.track_history
.set_items(vec![lidarr_history_item()]);
track_details_modal.track_history.search = Some("track history search".into());
track_details_modal.track_history.filter = Some("track history filter".into());
track_details_modal
.track_history
.sorting(vec![sort_option!(id)]);
let mut album_details_modal = AlbumDetailsModal {
track_details_modal: Some(track_details_modal),
..AlbumDetailsModal::default()
};
album_details_modal.tracks.set_items(vec![track()]);
album_details_modal.tracks.search = Some("album search".into());
album_details_modal
@@ -469,6 +484,8 @@ pub enum ActiveLidarrBlock {
FilterHistoryError,
FilterArtistHistory,
FilterArtistHistoryError,
FilterTrackHistory,
FilterTrackHistoryError,
History,
HistoryItemDetails,
HistorySortPrompt,
@@ -499,12 +516,18 @@ pub enum ActiveLidarrBlock {
SearchArtistHistoryError,
SearchTracks,
SearchTracksError,
SearchTrackHistory,
SearchTrackHistoryError,
System,
SystemLogs,
SystemQueuedEvents,
SystemTasks,
SystemTaskStartConfirmPrompt,
SystemUpdates,
TrackDetails,
TrackHistory,
TrackHistoryDetails,
TrackHistorySortPrompt,
UpdateAllArtistsPrompt,
UpdateAndScanArtistPrompt,
UpdateDownloadsPrompt,
@@ -767,6 +790,17 @@ pub static SYSTEM_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
ActiveLidarrBlock::SystemUpdates,
];
pub static TRACK_DETAILS_BLOCKS: [ActiveLidarrBlock; 8] = [
ActiveLidarrBlock::TrackDetails,
ActiveLidarrBlock::TrackHistory,
ActiveLidarrBlock::TrackHistoryDetails,
ActiveLidarrBlock::SearchTrackHistory,
ActiveLidarrBlock::SearchTrackHistoryError,
ActiveLidarrBlock::FilterTrackHistory,
ActiveLidarrBlock::FilterTrackHistoryError,
ActiveLidarrBlock::TrackHistorySortPrompt,
];
impl From<ActiveLidarrBlock> for Route {
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
Route::Lidarr(active_lidarr_block, None)
@@ -16,6 +16,7 @@ mod tests {
EDIT_ARTIST_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
TRACK_DETAILS_BLOCKS,
};
use crate::models::{
BlockSelectionState, Route,
@@ -695,4 +696,17 @@ mod tests {
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTaskStartConfirmPrompt));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemUpdates));
}
#[test]
fn test_track_details_blocks_contents() {
assert_eq!(TRACK_DETAILS_BLOCKS.len(), 8);
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackDetails));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistory));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistoryDetails));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTrackHistory));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTrackHistoryError));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterTrackHistory));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterTrackHistoryError));
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistorySortPrompt));
}
}
+34 -3
View File
@@ -1,13 +1,14 @@
use super::lidarr_data::{ActiveLidarrBlock, LidarrData};
use crate::app::lidarr::lidarr_context_clues::{
ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
};
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track, TrackFile};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::Indexer;
use crate::models::stateful_table::StatefulTable;
use crate::models::{
HorizontallyScrollableText, TabRoute, TabState,
HorizontallyScrollableText, ScrollableText, TabRoute, TabState,
lidarr_models::{MonitorType, NewItemMonitorType},
servarr_models::RootFolder,
stateful_list::StatefulList,
@@ -226,7 +227,7 @@ impl From<&LidarrData<'_>> for AddRootFolderModal {
pub struct AlbumDetailsModal {
pub tracks: StatefulTable<Track>,
pub track_files: StatefulTable<TrackFile>,
// pub track_details_modal: Option<EpisodeDetailsModal>,
pub track_details_modal: Option<TrackDetailsModal>,
pub album_history: StatefulTable<LidarrHistoryItem>,
pub album_releases: StatefulTable<LidarrRelease>,
pub album_details_tabs: TabState,
@@ -236,7 +237,7 @@ impl Default for AlbumDetailsModal {
fn default() -> AlbumDetailsModal {
AlbumDetailsModal {
tracks: StatefulTable::default(),
// TODO episode_details_modal: None,
track_details_modal: None,
track_files: StatefulTable::default(),
album_releases: StatefulTable::default(),
album_history: StatefulTable::default(),
@@ -263,3 +264,33 @@ impl Default for AlbumDetailsModal {
}
}
}
#[cfg_attr(test, derive(Debug))]
pub struct TrackDetailsModal {
pub track_details: ScrollableText,
pub track_history: StatefulTable<LidarrHistoryItem>,
pub track_details_tabs: TabState,
}
impl Default for TrackDetailsModal {
fn default() -> Self {
TrackDetailsModal {
track_details: ScrollableText::default(),
track_history: StatefulTable::default(),
track_details_tabs: TabState::new(vec![
TabRoute {
title: "Track Details".to_string(),
route: ActiveLidarrBlock::TrackDetails.into(),
contextual_help: Some(&TRACK_DETAILS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveLidarrBlock::TrackHistory.into(),
contextual_help: Some(&TRACK_HISTORY_CONTEXT_CLUES),
config: None,
},
]),
}
}
}
+57 -39
View File
@@ -2,11 +2,12 @@
mod tests {
use crate::app::lidarr::lidarr_context_clues::{
ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
};
use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
use crate::models::servarr_data::lidarr::modals::{
AddArtistModal, AlbumDetailsModal, EditArtistModal,
AddArtistModal, AlbumDetailsModal, EditArtistModal, TrackDetailsModal,
};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
@@ -92,14 +93,14 @@ mod tests {
quality_profile_id: 1,
metadata_profile_id: 1,
path: "/nfs/music/test_artist".to_owned(),
tags: vec![serde_json::Number::from(1)],
tags: vec![Number::from(1)],
..Artist::default()
};
lidarr_data.artists.set_items(vec![artist]);
let edit_artist_modal = EditArtistModal::from(&lidarr_data);
assert_eq!(edit_artist_modal.monitored, Some(true));
assert_some_eq_x!(&edit_artist_modal.monitored, &true);
assert_eq!(
*edit_artist_modal.monitor_list.current_selection(),
NewItemMonitorType::All
@@ -205,24 +206,24 @@ mod tests {
let edit_indexer_modal = EditIndexerModal::from(&lidarr_data);
assert_str_eq!(edit_indexer_modal.name.text, "Test");
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
assert_some_eq_x!(&edit_indexer_modal.enable_rss, &true);
assert_some_eq_x!(&edit_indexer_modal.enable_automatic_search, &true);
assert_some_eq_x!(&edit_indexer_modal.enable_interactive_search, &true);
assert_eq!(edit_indexer_modal.priority, 1);
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
assert_is_empty!(edit_indexer_modal.seed_ratio.text);
}
#[test]
fn test_album_details_modal_default() {
let album_details_modal = AlbumDetailsModal::default();
assert!(album_details_modal.tracks.is_empty());
// assert!(album_details_modal.track_details_modal.is_none());
assert!(album_details_modal.track_files.is_empty());
assert!(album_details_modal.album_releases.is_empty());
assert!(album_details_modal.album_history.is_empty());
assert_is_empty!(album_details_modal.tracks);
assert_none!(album_details_modal.track_details_modal);
assert_is_empty!(album_details_modal.track_files);
assert_is_empty!(album_details_modal.album_releases);
assert_is_empty!(album_details_modal.album_history);
assert_eq!(album_details_modal.album_details_tabs.tabs.len(), 3);
@@ -234,15 +235,8 @@ mod tests {
album_details_modal.album_details_tabs.tabs[0].route,
ActiveLidarrBlock::AlbumDetails.into()
);
assert!(
album_details_modal.album_details_tabs.tabs[0]
.contextual_help
.is_some()
);
assert_eq!(
album_details_modal.album_details_tabs.tabs[0]
.contextual_help
.unwrap(),
assert_some_eq_x!(
&album_details_modal.album_details_tabs.tabs[0].contextual_help,
&ALBUM_DETAILS_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[0].config, None);
@@ -255,15 +249,8 @@ mod tests {
album_details_modal.album_details_tabs.tabs[1].route,
ActiveLidarrBlock::AlbumHistory.into()
);
assert!(
album_details_modal.album_details_tabs.tabs[1]
.contextual_help
.is_some()
);
assert_eq!(
album_details_modal.album_details_tabs.tabs[1]
.contextual_help
.unwrap(),
assert_some_eq_x!(
&album_details_modal.album_details_tabs.tabs[1].contextual_help,
&ALBUM_HISTORY_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[1].config, None);
@@ -276,17 +263,48 @@ mod tests {
album_details_modal.album_details_tabs.tabs[2].route,
ActiveLidarrBlock::ManualAlbumSearch.into()
);
assert!(
album_details_modal.album_details_tabs.tabs[2]
.contextual_help
.is_some()
);
assert_eq!(
album_details_modal.album_details_tabs.tabs[2]
.contextual_help
.unwrap(),
assert_some_eq_x!(
&album_details_modal.album_details_tabs.tabs[2].contextual_help,
&MANUAL_ALBUM_SEARCH_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[2].config, None);
}
#[test]
fn test_track_details_modal_default() {
let track_details_modal = TrackDetailsModal::default();
assert_is_empty!(track_details_modal.track_details);
assert_is_empty!(track_details_modal.track_history);
assert_eq!(track_details_modal.track_details_tabs.tabs.len(), 2);
assert_str_eq!(
track_details_modal.track_details_tabs.tabs[0].title,
"Track Details"
);
assert_eq!(
track_details_modal.track_details_tabs.tabs[0].route,
ActiveLidarrBlock::TrackDetails.into()
);
assert_some_eq_x!(
&track_details_modal.track_details_tabs.tabs[0].contextual_help,
&TRACK_DETAILS_CONTEXT_CLUES
);
assert_eq!(track_details_modal.track_details_tabs.tabs[0].config, None);
assert_str_eq!(
track_details_modal.track_details_tabs.tabs[1].title,
"History"
);
assert_eq!(
track_details_modal.track_details_tabs.tabs[1].route,
ActiveLidarrBlock::TrackHistory.into()
);
assert_some_eq_x!(
&track_details_modal.track_details_tabs.tabs[1].contextual_help,
&TRACK_HISTORY_CONTEXT_CLUES
);
assert_eq!(track_details_modal.track_details_tabs.tabs[1].config, None);
}
}