feat: Added TUI and CLI support for viewing Artist history in Lidarr

This commit is contained in:
2026-01-14 16:09:37 -07:00
parent 8b9467bd39
commit d7f0dd5950
66 changed files with 1843 additions and 256 deletions
+2 -1
View File
@@ -479,7 +479,8 @@ serde_enum_from!(
Artists(Vec<Artist>),
DiskSpaces(Vec<DiskSpace>),
DownloadsResponse(DownloadsResponse),
HistoryWrapper(LidarrHistoryWrapper),
LidarrHistoryWrapper(LidarrHistoryWrapper),
LidarrHistoryItems(Vec<LidarrHistoryItem>),
HostConfig(HostConfig),
IndexerSettings(IndexerSettings),
Indexers(Vec<Indexer>),
+16 -2
View File
@@ -305,7 +305,7 @@ mod tests {
}
#[test]
fn test_lidarr_serdeable_from_history_wrapper() {
fn test_lidarr_serdeable_from_lidarr_history_wrapper() {
let history_wrapper = LidarrHistoryWrapper {
records: vec![LidarrHistoryItem {
id: 1,
@@ -317,7 +317,21 @@ mod tests {
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::HistoryWrapper(history_wrapper)
LidarrSerdeable::LidarrHistoryWrapper(history_wrapper)
);
}
#[test]
fn test_lidarr_serdeable_from_lidarr_history_items() {
let history_items = vec![LidarrHistoryItem {
id: 1,
..LidarrHistoryItem::default()
}];
let lidarr_serdeable: LidarrSerdeable = history_items.clone().into();
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::LidarrHistoryItems(history_items)
);
}
+41 -8
View File
@@ -6,7 +6,7 @@ use crate::app::context_clues::{
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
use crate::models::lidarr_models::LidarrTask;
use crate::models::servarr_data::modals::EditIndexerModal;
@@ -39,6 +39,7 @@ use {
crate::network::servarr_test_utils::indexer_test_result,
crate::network::servarr_test_utils::queued_event,
crate::network::sonarr_network::sonarr_network_test_utils::test_utils::updates,
crate::sort_option,
strum::{Display, EnumString, IntoEnumIterator},
};
@@ -53,6 +54,7 @@ pub struct LidarrData<'a> {
pub add_root_folder_modal: Option<AddRootFolderModal>,
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
pub albums: StatefulTable<Album>,
pub artist_history: Option<StatefulTable<LidarrHistoryItem>>,
pub artist_info_tabs: TabState,
pub artists: StatefulTable<Artist>,
pub delete_files: bool,
@@ -90,6 +92,7 @@ impl LidarrData<'_> {
pub fn reset_artist_info_tabs(&mut self) {
self.albums = StatefulTable::default();
self.artist_history = None;
self.artist_info_tabs.index = 0;
}
@@ -134,6 +137,7 @@ impl<'a> Default for LidarrData<'a> {
add_root_folder_modal: None,
add_searched_artists: None,
albums: StatefulTable::default(),
artist_history: None,
artists: StatefulTable::default(),
delete_files: false,
disk_space_vec: Vec::new(),
@@ -197,12 +201,20 @@ impl<'a> Default for LidarrData<'a> {
config: None,
},
]),
artist_info_tabs: TabState::new(vec![TabRoute {
title: "Albums".to_string(),
route: ActiveLidarrBlock::ArtistDetails.into(),
contextual_help: Some(&ARTIST_DETAILS_CONTEXT_CLUES),
config: None,
}]),
artist_info_tabs: TabState::new(vec![
TabRoute {
title: "Albums".to_string(),
route: ActiveLidarrBlock::ArtistDetails.into(),
contextual_help: Some(&ARTIST_DETAILS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveLidarrBlock::ArtistHistory.into(),
contextual_help: Some(&ARTIST_HISTORY_CONTEXT_CLUES),
config: None,
},
]),
}
}
}
@@ -279,7 +291,14 @@ impl LidarrData<'_> {
let mut indexer_test_all_results = StatefulTable::default();
indexer_test_all_results.set_items(vec![indexer_test_result()]);
let mut artist_history = StatefulTable::default();
artist_history.set_items(vec![lidarr_history_item()]);
artist_history.sorting(vec![sort_option!(id)]);
artist_history.search = Some("artist history search".into());
artist_history.filter = Some("artist history filter".into());
let mut lidarr_data = LidarrData {
artist_history: Some(artist_history),
delete_files: true,
disk_space_vec: vec![diskspace()],
quality_profile_map: quality_profile_map(),
@@ -335,6 +354,9 @@ pub enum ActiveLidarrBlock {
#[default]
Artists,
ArtistDetails,
ArtistHistory,
ArtistHistoryDetails,
ArtistHistorySortPrompt,
ArtistsSortPrompt,
AddArtistAlreadyInLibrary,
AddArtistConfirmPrompt,
@@ -394,6 +416,8 @@ pub enum ActiveLidarrBlock {
FilterArtistsError,
FilterHistory,
FilterHistoryError,
FilterArtistHistory,
FilterArtistHistoryError,
History,
HistoryItemDetails,
HistorySortPrompt,
@@ -412,6 +436,8 @@ pub enum ActiveLidarrBlock {
SearchArtistsError,
SearchHistory,
SearchHistoryError,
SearchArtistHistory,
SearchArtistHistoryError,
System,
SystemLogs,
SystemQueuedEvents,
@@ -433,11 +459,18 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::UpdateAllArtistsPrompt,
];
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 12] = [
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ArtistHistoryDetails,
ActiveLidarrBlock::ArtistHistorySortPrompt,
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::FilterArtistHistory,
ActiveLidarrBlock::FilterArtistHistoryError,
ActiveLidarrBlock::SearchAlbums,
ActiveLidarrBlock::SearchAlbumsError,
ActiveLidarrBlock::SearchArtistHistory,
ActiveLidarrBlock::SearchArtistHistoryError,
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
];
@@ -5,7 +5,7 @@ mod tests {
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
use crate::models::lidarr_models::Album;
use crate::models::servarr_data::lidarr::lidarr_data::{
@@ -16,6 +16,7 @@ mod tests {
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
use crate::models::{
BlockSelectionState, Route,
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS, LidarrData},
@@ -59,11 +60,13 @@ mod tests {
fn test_reset_artist_info_tabs() {
let mut lidarr_data = LidarrData::default();
lidarr_data.albums.set_items(vec![Album::default()]);
lidarr_data.artist_history = Some(StatefulTable::default());
lidarr_data.artist_info_tabs.index = 1;
lidarr_data.reset_artist_info_tabs();
assert_is_empty!(lidarr_data.albums);
assert_none!(lidarr_data.artist_history);
assert_eq!(lidarr_data.artist_info_tabs.index, 0);
}
@@ -137,6 +140,7 @@ mod tests {
assert_none!(lidarr_data.add_searched_artists);
assert_is_empty!(lidarr_data.albums);
assert_is_empty!(lidarr_data.artists);
assert_none!(lidarr_data.artist_history);
assert!(!lidarr_data.delete_files);
assert_is_empty!(lidarr_data.disk_space_vec);
assert_is_empty!(lidarr_data.downloads);
@@ -226,7 +230,7 @@ mod tests {
);
assert_none!(lidarr_data.main_tabs.tabs[5].config);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 2);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
assert_eq!(
lidarr_data.artist_info_tabs.tabs[0].route,
@@ -237,6 +241,17 @@ mod tests {
&ARTIST_DETAILS_CONTEXT_CLUES
);
assert_none!(lidarr_data.artist_info_tabs.tabs[0].config);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[1].title, "History");
assert_eq!(
lidarr_data.artist_info_tabs.tabs[1].route,
ActiveLidarrBlock::ArtistHistory.into()
);
assert_some_eq_x!(
&lidarr_data.artist_info_tabs.tabs[1].contextual_help,
&ARTIST_HISTORY_CONTEXT_CLUES
);
assert_none!(lidarr_data.artist_info_tabs.tabs[1].config);
}
#[test]
@@ -253,11 +268,18 @@ mod tests {
#[test]
fn test_artist_details_blocks_contains_expected_blocks() {
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 5);
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 12);
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistDetails));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistory));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistoryDetails));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistorySortPrompt));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchArtistPrompt));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistHistory));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistHistoryError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbums));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumsError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistHistory));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistHistoryError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
}
@@ -37,8 +37,8 @@ use {
crate::network::servarr_test_utils::indexer_test_result,
crate::network::servarr_test_utils::queued_event,
crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
add_series_search_result, blocklist_item, download_record, history_item, indexer, log_line,
root_folder,
add_series_search_result, blocklist_item, download_record, indexer, log_line, root_folder,
sonarr_history_item,
},
crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
episode, episode_file, language_profiles_map, quality_profile_map, season, series, tags_map,
@@ -309,7 +309,7 @@ impl SonarrData<'_> {
};
episode_details_modal
.episode_history
.set_items(vec![history_item()]);
.set_items(vec![sonarr_history_item()]);
episode_details_modal
.episode_releases
.set_items(vec![torrent_release(), usenet_release()]);
@@ -328,7 +328,7 @@ impl SonarrData<'_> {
.set_items(vec![episode_file()]);
season_details_modal
.season_history
.set_items(vec![history_item()]);
.set_items(vec![sonarr_history_item()]);
season_details_modal.season_history.search = Some("season history search".into());
season_details_modal.season_history.filter = Some("season history filter".into());
season_details_modal
@@ -342,7 +342,7 @@ impl SonarrData<'_> {
.sorting(vec![sort_option!(indexer_id)]);
let mut series_history = StatefulTable::default();
series_history.set_items(vec![history_item()]);
series_history.set_items(vec![sonarr_history_item()]);
series_history.sorting(vec![sort_option!(id)]);
series_history.search = Some("series history search".into());
series_history.filter = Some("series history filter".into());
@@ -374,7 +374,7 @@ impl SonarrData<'_> {
sonarr_data.blocklist.set_items(vec![blocklist_item()]);
sonarr_data.blocklist.sorting(vec![sort_option!(id)]);
sonarr_data.downloads.set_items(vec![download_record()]);
sonarr_data.history.set_items(vec![history_item()]);
sonarr_data.history.set_items(vec![sonarr_history_item()]);
sonarr_data.history.sorting(vec![sort_option!(id)]);
sonarr_data.history.search = Some("test search".into());
sonarr_data.history.filter = Some("test filter".into());