feat: Implemented the Lidarr History tab and CLI support

This commit is contained in:
2026-01-12 14:21:58 -07:00
parent f31810e48a
commit 68b08d1cd7
41 changed files with 2505 additions and 78 deletions
+76 -1
View File
@@ -7,7 +7,9 @@ use strum::{Display, EnumIter};
use super::{
HorizontallyScrollableText, Serdeable,
servarr_models::{DiskSpace, HostConfig, QualityProfile, RootFolder, SecurityConfig, Tag},
servarr_models::{
DiskSpace, HostConfig, QualityProfile, QualityWrapper, RootFolder, SecurityConfig, Tag,
},
};
use crate::serde_enum_from;
@@ -337,6 +339,78 @@ pub struct AlbumStatistics {
impl Eq for AlbumStatistics {}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LidarrHistoryWrapper {
pub records: Vec<LidarrHistoryItem>,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LidarrHistoryData {
pub indexer: Option<String>,
pub release_group: Option<String>,
pub nzb_info_url: Option<String>,
pub download_client_name: Option<String>,
pub download_client: Option<String>,
pub age: Option<String>,
pub published_date: Option<DateTime<Utc>>,
pub message: Option<String>,
pub reason: Option<String>,
pub dropped_path: Option<String>,
pub imported_path: Option<String>,
pub source_path: Option<String>,
pub path: Option<String>,
pub status_messages: Option<String>,
}
#[derive(
Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Display, EnumDisplayStyle,
)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum LidarrHistoryEventType {
#[default]
Unknown,
Grabbed,
#[display_style(name = "Artist Folder Imported")]
ArtistFolderImported,
#[display_style(name = "Album Import Incomplete")]
AlbumImportIncomplete,
#[display_style(name = "Download Ignored")]
DownloadIgnored,
#[display_style(name = "Download Imported")]
DownloadImported,
#[display_style(name = "Download Failed")]
DownloadFailed,
#[display_style(name = "Track File Deleted")]
TrackFileDeleted,
#[display_style(name = "Track File Imported")]
TrackFileImported,
#[display_style(name = "Track File Renamed")]
TrackFileRenamed,
#[display_style(name = "Track File Retagged")]
TrackFileRetagged,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LidarrHistoryItem {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
pub source_title: HorizontallyScrollableText,
#[serde(deserialize_with = "super::from_i64")]
pub album_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub artist_id: i64,
#[serde(default)]
pub quality: QualityWrapper,
pub date: DateTime<Utc>,
pub event_type: LidarrHistoryEventType,
#[serde(default)]
pub data: LidarrHistoryData,
}
impl From<LidarrSerdeable> for Serdeable {
fn from(value: LidarrSerdeable) -> Serdeable {
Serdeable::Lidarr(value)
@@ -352,6 +426,7 @@ serde_enum_from!(
Artists(Vec<Artist>),
DiskSpaces(Vec<DiskSpace>),
DownloadsResponse(DownloadsResponse),
HistoryWrapper(LidarrHistoryWrapper),
HostConfig(HostConfig),
MetadataProfiles(Vec<MetadataProfile>),
QualityProfiles(Vec<QualityProfile>),
+104 -2
View File
@@ -5,8 +5,9 @@ mod tests {
use serde_json::json;
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, Member, MetadataProfile,
MonitorType, NewItemMonitorType, SystemStatus,
};
use crate::models::servarr_models::{
DiskSpace, HostConfig, QualityProfile, RootFolder, SecurityConfig, Tag,
@@ -302,6 +303,23 @@ mod tests {
);
}
#[test]
fn test_lidarr_serdeable_from_history_wrapper() {
let history_wrapper = LidarrHistoryWrapper {
records: vec![LidarrHistoryItem {
id: 1,
..LidarrHistoryItem::default()
}],
};
let lidarr_serdeable: LidarrSerdeable = history_wrapper.clone().into();
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::HistoryWrapper(history_wrapper)
);
}
#[test]
fn test_lidarr_serdeable_from_metadata_profiles() {
let metadata_profiles = vec![MetadataProfile {
@@ -488,6 +506,90 @@ mod tests {
assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback");
}
#[test]
fn test_lidarr_history_event_type_display() {
assert_str_eq!(LidarrHistoryEventType::Unknown.to_string(), "unknown");
assert_str_eq!(LidarrHistoryEventType::Grabbed.to_string(), "grabbed");
assert_str_eq!(
LidarrHistoryEventType::ArtistFolderImported.to_string(),
"artistFolderImported"
);
assert_str_eq!(
LidarrHistoryEventType::AlbumImportIncomplete.to_string(),
"albumImportIncomplete"
);
assert_str_eq!(
LidarrHistoryEventType::DownloadIgnored.to_string(),
"downloadIgnored"
);
assert_str_eq!(
LidarrHistoryEventType::DownloadImported.to_string(),
"downloadImported"
);
assert_str_eq!(
LidarrHistoryEventType::DownloadFailed.to_string(),
"downloadFailed"
);
assert_str_eq!(
LidarrHistoryEventType::TrackFileDeleted.to_string(),
"trackFileDeleted"
);
assert_str_eq!(
LidarrHistoryEventType::TrackFileImported.to_string(),
"trackFileImported"
);
assert_str_eq!(
LidarrHistoryEventType::TrackFileRenamed.to_string(),
"trackFileRenamed"
);
assert_str_eq!(
LidarrHistoryEventType::TrackFileRetagged.to_string(),
"trackFileRetagged"
);
}
#[test]
fn test_lidarr_history_event_type_to_display_str() {
assert_str_eq!(LidarrHistoryEventType::Unknown.to_display_str(), "Unknown");
assert_str_eq!(LidarrHistoryEventType::Grabbed.to_display_str(), "Grabbed");
assert_str_eq!(
LidarrHistoryEventType::ArtistFolderImported.to_display_str(),
"Artist Folder Imported"
);
assert_str_eq!(
LidarrHistoryEventType::AlbumImportIncomplete.to_display_str(),
"Album Import Incomplete"
);
assert_str_eq!(
LidarrHistoryEventType::DownloadIgnored.to_display_str(),
"Download Ignored"
);
assert_str_eq!(
LidarrHistoryEventType::DownloadImported.to_display_str(),
"Download Imported"
);
assert_str_eq!(
LidarrHistoryEventType::DownloadFailed.to_display_str(),
"Download Failed"
);
assert_str_eq!(
LidarrHistoryEventType::TrackFileDeleted.to_display_str(),
"Track File Deleted"
);
assert_str_eq!(
LidarrHistoryEventType::TrackFileImported.to_display_str(),
"Track File Imported"
);
assert_str_eq!(
LidarrHistoryEventType::TrackFileRenamed.to_display_str(),
"Track File Renamed"
);
assert_str_eq!(
LidarrHistoryEventType::TrackFileRetagged.to_display_str(),
"Track File Retagged"
);
}
#[test]
fn test_add_artist_search_result_deserialization() {
let search_result_json = json!({
+44 -9
View File
@@ -1,12 +1,13 @@
use serde_json::Number;
use super::modals::{AddArtistModal, EditArtistModal};
use crate::app::context_clues::HISTORY_CONTEXT_CLUES;
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord},
lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord, LidarrHistoryItem},
servarr_models::{DiskSpace, RootFolder},
stateful_table::StatefulTable,
};
@@ -21,8 +22,8 @@ use {
crate::models::stateful_table::SortOption,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
add_artist_search_result, album, artist, download_record, metadata_profile,
metadata_profile_map, quality_profile, root_folder, tags_map,
add_artist_search_result, album, artist, download_record, lidarr_history_item,
metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map,
},
crate::network::servarr_test_utils::diskspace,
strum::{Display, EnumString, IntoEnumIterator},
@@ -44,6 +45,7 @@ pub struct LidarrData<'a> {
pub disk_space_vec: Vec<DiskSpace>,
pub downloads: StatefulTable<DownloadRecord>,
pub edit_artist_modal: Option<EditArtistModal>,
pub history: StatefulTable<LidarrHistoryItem>,
pub main_tabs: TabState,
pub metadata_profile_map: BiMap<i64, String>,
pub prompt_confirm: bool,
@@ -112,6 +114,7 @@ impl<'a> Default for LidarrData<'a> {
disk_space_vec: Vec::new(),
downloads: StatefulTable::default(),
edit_artist_modal: None,
history: StatefulTable::default(),
metadata_profile_map: BiMap::new(),
prompt_confirm: false,
prompt_confirm_action: None,
@@ -121,12 +124,20 @@ impl<'a> Default for LidarrData<'a> {
start_time: DateTime::default(),
tags_map: BiMap::new(),
version: String::new(),
main_tabs: TabState::new(vec![TabRoute {
title: "Library".to_string(),
route: ActiveLidarrBlock::Artists.into(),
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
config: None,
}]),
main_tabs: TabState::new(vec![
TabRoute {
title: "Library".to_string(),
route: ActiveLidarrBlock::Artists.into(),
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveLidarrBlock::History.into(),
contextual_help: Some(&HISTORY_CONTEXT_CLUES),
config: None,
},
]),
artist_info_tabs: TabState::new(vec![TabRoute {
title: "Albums".to_string(),
route: ActiveLidarrBlock::ArtistDetails.into(),
@@ -195,6 +206,13 @@ impl LidarrData<'_> {
lidarr_data.artists.search = Some("artist search".into());
lidarr_data.artists.filter = Some("artist filter".into());
lidarr_data.downloads.set_items(vec![download_record()]);
lidarr_data.history.set_items(vec![lidarr_history_item()]);
lidarr_data.history.sorting(vec![SortOption {
name: "Date",
cmp_fn: Some(|a: &LidarrHistoryItem, b: &LidarrHistoryItem| a.date.cmp(&b.date)),
}]);
lidarr_data.history.search = Some("test search".into());
lidarr_data.history.filter = Some("test filter".into());
lidarr_data.root_folders.set_items(vec![root_folder()]);
lidarr_data.version = "1.0.0".to_owned();
lidarr_data.add_artist_search = Some("Test Artist".into());
@@ -244,10 +262,17 @@ pub enum ActiveLidarrBlock {
EditArtistToggleMonitored,
FilterArtists,
FilterArtistsError,
FilterHistory,
FilterHistoryError,
History,
HistoryItemDetails,
HistorySortPrompt,
SearchAlbums,
SearchAlbumsError,
SearchArtists,
SearchArtistsError,
SearchHistory,
SearchHistoryError,
UpdateAllArtistsPrompt,
UpdateAndScanArtistPrompt,
}
@@ -270,6 +295,16 @@ pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
];
pub static HISTORY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::History,
ActiveLidarrBlock::HistoryItemDetails,
ActiveLidarrBlock::HistorySortPrompt,
ActiveLidarrBlock::SearchHistory,
ActiveLidarrBlock::SearchHistoryError,
ActiveLidarrBlock::FilterHistory,
ActiveLidarrBlock::FilterHistoryError,
];
pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 12] = [
ActiveLidarrBlock::AddArtistAlreadyInLibrary,
ActiveLidarrBlock::AddArtistConfirmPrompt,
@@ -1,5 +1,6 @@
#[cfg(test)]
mod tests {
use crate::app::context_clues::HISTORY_CONTEXT_CLUES;
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
@@ -7,7 +8,7 @@ mod tests {
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS,
DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_BLOCKS,
};
use crate::models::{
BlockSelectionState, Route,
@@ -134,6 +135,7 @@ mod tests {
assert_is_empty!(lidarr_data.disk_space_vec);
assert_is_empty!(lidarr_data.downloads);
assert_none!(lidarr_data.edit_artist_modal);
assert_is_empty!(lidarr_data.history);
assert_is_empty!(lidarr_data.metadata_profile_map);
assert!(!lidarr_data.prompt_confirm);
assert_none!(lidarr_data.prompt_confirm_action);
@@ -144,7 +146,7 @@ mod tests {
assert_is_empty!(lidarr_data.tags_map);
assert_is_empty!(lidarr_data.version);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 1);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 2);
assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
@@ -157,6 +159,17 @@ mod tests {
);
assert_none!(lidarr_data.main_tabs.tabs[0].config);
assert_str_eq!(lidarr_data.main_tabs.tabs[1].title, "History");
assert_eq!(
lidarr_data.main_tabs.tabs[1].route,
ActiveLidarrBlock::History.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[1].contextual_help,
&HISTORY_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[1].config);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
assert_eq!(
@@ -192,6 +205,18 @@ mod tests {
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
}
#[test]
fn test_history_blocks_contains_expected_blocks() {
assert_eq!(HISTORY_BLOCKS.len(), 7);
assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::History));
assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::HistoryItemDetails));
assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::HistorySortPrompt));
assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::SearchHistory));
assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::SearchHistoryError));
assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::FilterHistory));
assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::FilterHistoryError));
}
#[test]
fn test_add_artist_blocks_contents() {
assert_eq!(ADD_ARTIST_BLOCKS.len(), 12);
@@ -2,12 +2,11 @@ use super::modals::{AddSeriesModal, EditSeriesModal, SeasonDetailsModal};
use crate::{
app::{
context_clues::{
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
},
sonarr::sonarr_context_clues::{
HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
SERIES_HISTORY_CONTEXT_CLUES,
SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES,
},
},
models::{
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
mod sonarr_data_tests {
use crate::app::context_clues::HISTORY_CONTEXT_CLUES;
use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES;
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
use crate::models::stateful_table::StatefulTable;
@@ -10,9 +11,7 @@ mod tests {
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
},
sonarr::sonarr_context_clues::{
HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
},
sonarr::sonarr_context_clues::{SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES},
},
models::{
BlockSelectionState, Route,