feat: Bulk added CLI support for tracks and album functionalities in Lidarr

This commit is contained in:
2026-01-16 14:38:08 -07:00
parent 5e70d70758
commit bc6ecc39f4
26 changed files with 2058 additions and 34 deletions
+66
View File
@@ -256,6 +256,8 @@ pub struct LidarrCommandBody {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub artist_id: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub album_ids: Option<Vec<i64>>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
@@ -495,6 +497,67 @@ pub struct LidarrReleaseDownloadBody {
pub indexer_id: i64,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TrackFile {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
pub path: String,
#[serde(deserialize_with = "super::from_i64")]
pub size: i64,
pub quality: QualityWrapper,
pub date_added: DateTime<Utc>,
pub media_info: Option<MediaInfo>,
pub audio_tags: Option<AudioTags>,
}
#[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct MediaInfo {
pub audio_bitrate: Option<String>,
#[serde(deserialize_with = "super::from_i64")]
pub audio_channels: i64,
pub audio_codec: Option<String>,
pub audio_bits: Option<String>,
pub audio_sample_rate: Option<String>,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AudioTags {
pub title: String,
pub artist_title: String,
pub album_title: String,
#[serde(deserialize_with = "super::from_i64")]
pub disc_number: i64,
#[serde(deserialize_with = "super::from_i64")]
pub disc_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub year: i64,
pub duration: String,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Track {
pub id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub artist_id: i64,
pub foreign_track_id: String,
#[serde(deserialize_with = "super::from_i64")]
pub track_file_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub album_id: i64,
pub explicit: bool,
pub track_number: String,
pub title: String,
#[serde(deserialize_with = "super::from_i64")]
pub duration: i64,
pub has_file: bool,
pub ratings: Ratings,
}
impl From<LidarrSerdeable> for Serdeable {
fn from(value: LidarrSerdeable) -> Serdeable {
Serdeable::Lidarr(value)
@@ -527,6 +590,9 @@ serde_enum_from!(
Tag(Tag),
Tags(Vec<Tag>),
Tasks(Vec<LidarrTask>),
Track(Track),
Tracks(Vec<Track>),
TrackFiles(Vec<TrackFile>),
Updates(Vec<Update>),
Value(Value),
}
+47 -2
View File
@@ -5,9 +5,10 @@ mod tests {
use serde_json::json;
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse,
AddArtistSearchResult, Album, AudioTags, DownloadRecord, DownloadStatus, DownloadsResponse,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrRelease, LidarrTask,
Member, MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
MediaInfo, Member, MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus, Track,
TrackFile,
};
use crate::models::servarr_models::{
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
@@ -577,6 +578,50 @@ mod tests {
assert_eq!(lidarr_serdeable, LidarrSerdeable::Updates(updates));
}
#[test]
fn test_lidarr_serdeable_from_track_file() {
let track_files = vec![TrackFile {
id: 1,
media_info: Some(MediaInfo {
audio_channels: 2,
..MediaInfo::default()
}),
audio_tags: Some(AudioTags {
disc_number: 1,
..AudioTags::default()
}),
..TrackFile::default()
}];
let lidarr_serdeable: LidarrSerdeable = track_files.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::TrackFiles(track_files));
}
#[test]
fn test_lidarr_serdeable_from_track() {
let track = Track {
id: 1,
..Track::default()
};
let lidarr_serdeable: LidarrSerdeable = track.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Track(track));
}
#[test]
fn test_lidarr_serdeable_from_tracks() {
let tracks = vec![Track {
id: 1,
..Track::default()
}];
let lidarr_serdeable: LidarrSerdeable = tracks.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Tracks(tracks));
}
#[test]
fn test_artist_status_display() {
assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing");
+62 -4
View File
@@ -1,6 +1,6 @@
use serde_json::Number;
use super::modals::{AddArtistModal, AddRootFolderModal, EditArtistModal};
use super::modals::{AddArtistModal, AddRootFolderModal, AlbumDetailsModal, EditArtistModal};
use crate::app::context_clues::{
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
@@ -39,6 +39,7 @@ use {
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
torrent_release, usenet_release,
},
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{track, track_file},
crate::network::servarr_test_utils::diskspace,
crate::network::servarr_test_utils::indexer_test_result,
crate::network::servarr_test_utils::queued_event,
@@ -58,6 +59,7 @@ pub struct LidarrData<'a> {
pub add_root_folder_modal: Option<AddRootFolderModal>,
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
pub albums: StatefulTable<Album>,
pub album_details_modal: Option<AlbumDetailsModal>,
pub artist_history: StatefulTable<LidarrHistoryItem>,
pub artist_info_tabs: TabState,
pub artists: StatefulTable<Artist>,
@@ -143,6 +145,7 @@ impl<'a> Default for LidarrData<'a> {
add_root_folder_modal: None,
add_searched_artists: None,
albums: StatefulTable::default(),
album_details_modal: None,
artist_history: StatefulTable::default(),
artists: StatefulTable::default(),
delete_files: false,
@@ -289,6 +292,27 @@ impl LidarrData<'_> {
.metadata_profile_list
.set_items(vec![metadata_profile().name]);
let mut album_details_modal = AlbumDetailsModal::default();
album_details_modal.tracks.set_items(vec![track()]);
album_details_modal.tracks.search = Some("album search".into());
album_details_modal
.track_files
.set_items(vec![track_file()]);
album_details_modal
.album_history
.set_items(vec![lidarr_history_item()]);
album_details_modal.album_history.search = Some("album history search".into());
album_details_modal.album_history.filter = Some("album history filter".into());
album_details_modal
.album_history
.sorting(vec![sort_option!(id)]);
album_details_modal
.album_releases
.set_items(vec![torrent_release(), usenet_release()]);
album_details_modal
.album_releases
.sorting(vec![sort_option!(indexer_id)]);
let edit_indexer_modal = EditIndexerModal {
name: "DrunkenSlug".into(),
enable_rss: Some(true),
@@ -305,6 +329,7 @@ impl LidarrData<'_> {
indexer_test_all_results.set_items(vec![indexer_test_result()]);
let mut lidarr_data = LidarrData {
album_details_modal: Some(album_details_modal),
delete_files: true,
disk_space_vec: vec![diskspace()],
quality_profile_map: quality_profile_map(),
@@ -376,9 +401,6 @@ pub enum ActiveLidarrBlock {
ArtistHistoryDetails,
ArtistHistorySortPrompt,
ArtistsSortPrompt,
ManualArtistSearch,
ManualArtistSearchConfirmPrompt,
ManualArtistSearchSortPrompt,
AddArtistAlreadyInLibrary,
AddArtistConfirmPrompt,
AddArtistEmptySearchResults,
@@ -400,7 +422,12 @@ pub enum ActiveLidarrBlock {
AddRootFolderSelectQualityProfile,
AddRootFolderSelectMetadataProfile,
AddRootFolderTagsInput,
AlbumDetails,
AlbumHistory,
AlbumHistoryDetails,
AlbumHistorySortPrompt,
AllIndexerSettingsPrompt,
AutomaticallySearchAlbumPrompt,
AutomaticallySearchArtistPrompt,
DeleteAlbumPrompt,
DeleteAlbumConfirmPrompt,
@@ -410,6 +437,7 @@ pub enum ActiveLidarrBlock {
DeleteArtistConfirmPrompt,
DeleteArtistToggleDeleteFile,
DeleteArtistToggleAddListExclusion,
DeleteTrackFilePrompt,
DeleteDownloadPrompt,
DeleteRootFolderPrompt,
Downloads,
@@ -433,6 +461,8 @@ pub enum ActiveLidarrBlock {
EditIndexerPriorityInput,
EditIndexerTagsInput,
DeleteIndexerPrompt,
FilterAlbumHistory,
FilterAlbumHistoryError,
FilterArtists,
FilterArtistsError,
FilterHistory,
@@ -448,9 +478,17 @@ pub enum ActiveLidarrBlock {
IndexerSettingsMinimumAgeInput,
IndexerSettingsRetentionInput,
IndexerSettingsRssSyncIntervalInput,
ManualAlbumSearch,
ManualAlbumSearchConfirmPrompt,
ManualAlbumSearchSortPrompt,
ManualArtistSearch,
ManualArtistSearchConfirmPrompt,
ManualArtistSearchSortPrompt,
TestAllIndexers,
TestIndexer,
RootFolders,
SearchAlbumHistory,
SearchAlbumHistoryError,
SearchAlbums,
SearchAlbumsError,
SearchArtists,
@@ -459,6 +497,8 @@ pub enum ActiveLidarrBlock {
SearchHistoryError,
SearchArtistHistory,
SearchArtistHistoryError,
SearchTracks,
SearchTracksError,
System,
SystemLogs,
SystemQueuedEvents,
@@ -498,6 +538,24 @@ pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 15] = [
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
];
pub static ALBUM_DETAILS_BLOCKS: [ActiveLidarrBlock; 15] = [
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::ManualAlbumSearchConfirmPrompt,
ActiveLidarrBlock::ManualAlbumSearchSortPrompt,
ActiveLidarrBlock::DeleteTrackFilePrompt,
];
pub static DOWNLOADS_BLOCKS: [ActiveLidarrBlock; 3] = [
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
@@ -10,9 +10,9 @@ mod tests {
};
use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ARTIST_DETAILS_BLOCKS,
DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS,
DELETE_ARTIST_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS,
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS,
ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS,
DELETE_ARTIST_BLOCKS, 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, SYSTEM_DETAILS_BLOCKS,
@@ -145,6 +145,7 @@ mod tests {
assert!(!lidarr_data.add_import_list_exclusion);
assert_none!(lidarr_data.add_searched_artists);
assert_is_empty!(lidarr_data.albums);
assert_none!(lidarr_data.album_details_modal);
assert_is_empty!(lidarr_data.artists);
assert_is_empty!(lidarr_data.artist_history);
assert!(!lidarr_data.delete_files);
@@ -304,6 +305,26 @@ mod tests {
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
}
#[test]
fn test_album_details_blocks_contents() {
assert_eq!(ALBUM_DETAILS_BLOCKS.len(), 15);
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AlbumDetails));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AlbumHistory));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTracks));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTracksError));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchAlbumPrompt));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumHistory));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumHistoryError));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterAlbumHistory));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterAlbumHistoryError));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AlbumHistorySortPrompt));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AlbumHistoryDetails));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualAlbumSearch));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualAlbumSearchSortPrompt));
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::DeleteTrackFilePrompt));
}
#[test]
fn test_downloads_blocks_contains_expected_blocks() {
assert_eq!(DOWNLOADS_BLOCKS.len(), 3);
+50 -4
View File
@@ -1,14 +1,18 @@
use strum::IntoEnumIterator;
use super::lidarr_data::LidarrData;
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,
};
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,
HorizontallyScrollableText, TabRoute, TabState,
lidarr_models::{MonitorType, NewItemMonitorType},
servarr_models::RootFolder,
stateful_list::StatefulList,
};
use strum::IntoEnumIterator;
#[cfg(test)]
#[path = "modals_tests.rs"]
@@ -217,3 +221,45 @@ impl From<&LidarrData<'_>> for AddRootFolderModal {
add_root_folder_modal
}
}
#[cfg_attr(test, derive(Debug))]
pub struct AlbumDetailsModal {
pub tracks: StatefulTable<Track>,
pub track_files: StatefulTable<TrackFile>,
// pub track_details_modal: Option<EpisodeDetailsModal>,
pub album_history: StatefulTable<LidarrHistoryItem>,
pub album_releases: StatefulTable<LidarrRelease>,
pub album_details_tabs: TabState,
}
impl Default for AlbumDetailsModal {
fn default() -> AlbumDetailsModal {
AlbumDetailsModal {
tracks: StatefulTable::default(),
// TODO episode_details_modal: None,
track_files: StatefulTable::default(),
album_releases: StatefulTable::default(),
album_history: StatefulTable::default(),
album_details_tabs: TabState::new(vec![
TabRoute {
title: "Tracks".to_string(),
route: ActiveLidarrBlock::AlbumDetails.into(),
contextual_help: Some(&ALBUM_DETAILS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveLidarrBlock::AlbumHistory.into(),
contextual_help: Some(&ALBUM_HISTORY_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "Manual Search".to_string(),
route: ActiveLidarrBlock::ManualAlbumSearch.into(),
contextual_help: Some(&MANUAL_ALBUM_SEARCH_CONTEXT_CLUES),
config: None,
},
]),
}
}
}
+83 -2
View File
@@ -1,8 +1,13 @@
#[cfg(test)]
mod tests {
use crate::app::lidarr::lidarr_context_clues::{
ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
};
use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType};
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
use crate::models::servarr_data::lidarr::modals::{AddArtistModal, EditArtistModal};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
use crate::models::servarr_data::lidarr::modals::{
AddArtistModal, AlbumDetailsModal, EditArtistModal,
};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
use bimap::BiMap;
@@ -208,4 +213,80 @@ mod tests {
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
}
#[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_eq!(album_details_modal.album_details_tabs.tabs.len(), 3);
assert_str_eq!(
album_details_modal.album_details_tabs.tabs[0].title,
"Tracks"
);
assert_eq!(
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(),
&ALBUM_DETAILS_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[0].config, None);
assert_str_eq!(
album_details_modal.album_details_tabs.tabs[1].title,
"History"
);
assert_eq!(
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(),
&ALBUM_HISTORY_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[1].config, None);
assert_str_eq!(
album_details_modal.album_details_tabs.tabs[2].title,
"Manual Search"
);
assert_eq!(
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(),
&MANUAL_ALBUM_SEARCH_CONTEXT_CLUES
);
assert_eq!(album_details_modal.album_details_tabs.tabs[2].config, None);
}
}