feat: Full Lidarr system support for both the CLI and TUI

This commit is contained in:
2026-01-14 14:50:33 -07:00
parent c74d5936d2
commit 8b9467bd39
63 changed files with 4824 additions and 74 deletions
+43 -1
View File
@@ -5,13 +5,15 @@ use super::{
SecurityConfig, Tag,
},
};
use crate::models::servarr_models::IndexerSettings;
use crate::models::servarr_models::{IndexerSettings, LogResponse, QueueEvent, Update};
use crate::serde_enum_from;
use chrono::{DateTime, Utc};
use clap::ValueEnum;
use derivative::Derivative;
use enum_display_style_derive::EnumDisplayStyle;
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use std::fmt::{Display, Formatter};
use strum::{Display, EnumIter};
#[cfg(test)]
@@ -426,6 +428,42 @@ pub struct LidarrHistoryItem {
pub data: LidarrHistoryData,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LidarrTask {
pub name: String,
pub task_name: LidarrTaskName,
#[serde(deserialize_with = "super::from_i64")]
pub interval: i64,
pub last_execution: DateTime<Utc>,
pub next_execution: DateTime<Utc>,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)]
#[serde(rename_all = "PascalCase")]
pub enum LidarrTaskName {
#[default]
ApplicationUpdateCheck,
Backup,
CheckHealth,
Housekeeping,
ImportListSync,
MessagingCleanup,
RefreshArtist,
RefreshMonitoredDownloads,
RescanFolders,
RssSync,
}
impl Display for LidarrTaskName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let task_name = serde_json::to_string(&self)
.expect("Unable to serialize task name")
.replace('"', "");
write!(f, "{task_name}")
}
}
impl From<LidarrSerdeable> for Serdeable {
fn from(value: LidarrSerdeable) -> Serdeable {
Serdeable::Lidarr(value)
@@ -446,13 +484,17 @@ serde_enum_from!(
IndexerSettings(IndexerSettings),
Indexers(Vec<Indexer>),
IndexerTestResults(Vec<IndexerTestResult>),
LogResponse(LogResponse),
MetadataProfiles(Vec<MetadataProfile>),
QualityProfiles(Vec<QualityProfile>),
QueueEvents(Vec<QueueEvent>),
RootFolders(Vec<RootFolder>),
SecurityConfig(SecurityConfig),
SystemStatus(SystemStatus),
Tag(Tag),
Tags(Vec<Tag>),
Tasks(Vec<LidarrTask>),
Updates(Vec<Update>),
Value(Value),
}
);
+54 -4
View File
@@ -6,12 +6,12 @@ mod tests {
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, Member, MetadataProfile,
MonitorType, NewItemMonitorType, SystemStatus,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, Member,
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
};
use crate::models::servarr_models::{
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, QualityProfile, RootFolder,
SecurityConfig, Tag,
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
};
use crate::models::{
Serdeable,
@@ -363,6 +363,20 @@ mod tests {
);
}
#[test]
fn test_lidarr_serdeable_from_log_response() {
let log_response = LogResponse {
records: vec![Log {
level: "info".to_owned(),
..Log::default()
}],
};
let lidarr_serdeable: LidarrSerdeable = log_response.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::LogResponse(log_response));
}
#[test]
fn test_lidarr_serdeable_from_metadata_profiles() {
let metadata_profiles = vec![MetadataProfile {
@@ -405,6 +419,18 @@ mod tests {
);
}
#[test]
fn test_lidarr_serdeable_from_queue_events() {
let queue_events = vec![QueueEvent {
trigger: "test".to_owned(),
..QueueEvent::default()
}];
let lidarr_serdeable: LidarrSerdeable = queue_events.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::QueueEvents(queue_events));
}
#[test]
fn test_lidarr_serdeable_from_root_folders() {
let root_folders = vec![RootFolder {
@@ -501,6 +527,30 @@ mod tests {
assert_eq!(lidarr_serdeable, LidarrSerdeable::Album(album));
}
#[test]
fn test_lidarr_serdeable_from_tasks() {
let tasks = vec![LidarrTask {
name: "test".to_owned(),
..LidarrTask::default()
}];
let lidarr_serdeable: LidarrSerdeable = tasks.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Tasks(tasks));
}
#[test]
fn test_lidarr_serdeable_from_updates() {
let updates = vec![Update {
version: "test".to_owned(),
..Update::default()
}];
let lidarr_serdeable: LidarrSerdeable = updates.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Updates(updates));
}
#[test]
fn test_artist_status_display() {
assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing");
+45 -4
View File
@@ -3,15 +3,17 @@ use serde_json::Number;
use super::modals::{AddArtistModal, AddRootFolderModal, EditArtistModal};
use crate::app::context_clues::{
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
use crate::models::lidarr_models::LidarrTask;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::IndexerSettings;
use crate::models::servarr_models::{IndexerSettings, QueueEvent};
use crate::models::stateful_list::StatefulList;
use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState,
lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord, LidarrHistoryItem},
servarr_data::modals::IndexerTestResultModalItem,
servarr_models::{DiskSpace, Indexer, RootFolder},
@@ -32,8 +34,11 @@ use {
add_artist_search_result, album, artist, download_record, indexer, lidarr_history_item,
metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map,
},
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{log_line, task},
crate::network::servarr_test_utils::diskspace,
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,
strum::{Display, EnumString, IntoEnumIterator},
};
@@ -60,15 +65,20 @@ pub struct LidarrData<'a> {
pub indexer_settings: Option<IndexerSettings>,
pub indexer_test_all_results: Option<StatefulTable<IndexerTestResultModalItem>>,
pub indexer_test_errors: Option<String>,
pub logs: StatefulList<HorizontallyScrollableText>,
pub log_details: StatefulList<HorizontallyScrollableText>,
pub main_tabs: TabState,
pub metadata_profile_map: BiMap<i64, String>,
pub prompt_confirm: bool,
pub prompt_confirm_action: Option<LidarrEvent>,
pub quality_profile_map: BiMap<i64, String>,
pub queued_events: StatefulTable<QueueEvent>,
pub root_folders: StatefulTable<RootFolder>,
pub selected_block: BlockSelectionState<'a, ActiveLidarrBlock>,
pub start_time: DateTime<Utc>,
pub tags_map: BiMap<i64, String>,
pub tasks: StatefulTable<LidarrTask>,
pub updates: ScrollableText,
pub version: String,
}
@@ -135,14 +145,19 @@ impl<'a> Default for LidarrData<'a> {
indexer_settings: None,
indexer_test_all_results: None,
indexer_test_errors: None,
logs: StatefulList::default(),
log_details: StatefulList::default(),
metadata_profile_map: BiMap::new(),
prompt_confirm: false,
prompt_confirm_action: None,
quality_profile_map: BiMap::new(),
queued_events: StatefulTable::default(),
root_folders: StatefulTable::default(),
selected_block: BlockSelectionState::default(),
start_time: DateTime::default(),
tags_map: BiMap::new(),
tasks: StatefulTable::default(),
updates: ScrollableText::default(),
version: String::new(),
main_tabs: TabState::new(vec![
TabRoute {
@@ -175,6 +190,12 @@ impl<'a> Default for LidarrData<'a> {
contextual_help: Some(&INDEXERS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "System".to_string(),
route: ActiveLidarrBlock::System.into(),
contextual_help: Some(&SYSTEM_CONTEXT_CLUES),
config: None,
},
]),
artist_info_tabs: TabState::new(vec![TabRoute {
title: "Albums".to_string(),
@@ -270,7 +291,10 @@ impl LidarrData<'_> {
indexer_settings: Some(indexer_settings()),
indexer_test_all_results: Some(indexer_test_all_results),
indexer_test_errors: Some("error".to_string()),
start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()),
tags_map: tags_map(),
updates: updates(),
version: "1.2.3.4".to_owned(),
..LidarrData::default()
};
lidarr_data.albums.set_items(vec![album()]);
@@ -292,11 +316,14 @@ impl LidarrData<'_> {
lidarr_data.history.filter = Some("test filter".into());
lidarr_data.root_folders.set_items(vec![root_folder()]);
lidarr_data.indexers.set_items(vec![indexer()]);
lidarr_data.version = "1.0.0".to_owned();
lidarr_data.queued_events.set_items(vec![queued_event()]);
lidarr_data.add_artist_search = Some("Test Artist".into());
let mut add_searched_artists = StatefulTable::default();
add_searched_artists.set_items(vec![add_artist_search_result()]);
lidarr_data.add_searched_artists = Some(add_searched_artists);
lidarr_data.logs.set_items(vec![log_line().into()]);
lidarr_data.log_details.set_items(vec![log_line().into()]);
lidarr_data.tasks.set_items(vec![task()]);
lidarr_data
}
@@ -385,6 +412,12 @@ pub enum ActiveLidarrBlock {
SearchArtistsError,
SearchHistory,
SearchHistoryError,
System,
SystemLogs,
SystemQueuedEvents,
SystemTasks,
SystemTaskStartConfirmPrompt,
SystemUpdates,
UpdateAllArtistsPrompt,
UpdateAndScanArtistPrompt,
UpdateDownloadsPrompt,
@@ -611,6 +644,14 @@ pub static INDEXERS_BLOCKS: [ActiveLidarrBlock; 3] = [
ActiveLidarrBlock::TestIndexer,
];
pub static SYSTEM_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
ActiveLidarrBlock::SystemLogs,
ActiveLidarrBlock::SystemQueuedEvents,
ActiveLidarrBlock::SystemTasks,
ActiveLidarrBlock::SystemTaskStartConfirmPrompt,
ActiveLidarrBlock::SystemUpdates,
];
impl From<ActiveLidarrBlock> for Route {
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
Route::Lidarr(active_lidarr_block, None)
@@ -2,7 +2,7 @@
mod tests {
use crate::app::context_clues::{
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
@@ -14,7 +14,7 @@ mod tests {
DELETE_ARTIST_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS,
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,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
};
use crate::models::{
BlockSelectionState, Route,
@@ -143,17 +143,22 @@ mod tests {
assert_none!(lidarr_data.edit_artist_modal);
assert_none!(lidarr_data.add_root_folder_modal);
assert_is_empty!(lidarr_data.history);
assert_is_empty!(lidarr_data.logs);
assert_is_empty!(lidarr_data.log_details);
assert_is_empty!(lidarr_data.metadata_profile_map);
assert!(!lidarr_data.prompt_confirm);
assert_none!(lidarr_data.prompt_confirm_action);
assert_is_empty!(lidarr_data.quality_profile_map);
assert_is_empty!(lidarr_data.queued_events);
assert_is_empty!(lidarr_data.root_folders);
assert_eq!(lidarr_data.selected_block, BlockSelectionState::default());
assert_eq!(lidarr_data.start_time, <DateTime<Utc>>::default());
assert_is_empty!(lidarr_data.tags_map);
assert_is_empty!(lidarr_data.tasks);
assert_is_empty!(lidarr_data.updates);
assert_is_empty!(lidarr_data.version);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 5);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 6);
assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
@@ -210,6 +215,17 @@ mod tests {
);
assert_none!(lidarr_data.main_tabs.tabs[4].config);
assert_str_eq!(lidarr_data.main_tabs.tabs[5].title, "System");
assert_eq!(
lidarr_data.main_tabs.tabs[5].route,
ActiveLidarrBlock::System.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[5].contextual_help,
&SYSTEM_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[5].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!(
@@ -605,4 +621,14 @@ mod tests {
);
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderTagsInput));
}
#[test]
fn test_system_details_blocks_contents() {
assert_eq!(SYSTEM_DETAILS_BLOCKS.len(), 5);
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemLogs));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemQueuedEvents));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTasks));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTaskStartConfirmPrompt));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemUpdates));
}
}