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
@@ -0,0 +1,199 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrHistoryWrapper, LidarrSerdeable};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::SortOption;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::lidarr_history_item;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use rstest::rstest;
use serde_json::json;
#[rstest]
#[tokio::test]
async fn test_handle_get_lidarr_history_event(#[values(true, false)] use_custom_sorting: bool) {
let history_json = json!({"records": [{
"id": 123,
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]});
let response: LidarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("pageSize=500&sortDirection=descending&sortKey=date")
.build_for(LidarrEvent::GetHistory(500))
.await;
let mut expected_history_items = vec![
LidarrHistoryItem {
id: 123,
album_id: 1007,
artist_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
LidarrHistoryItem {
id: 456,
album_id: 2001,
artist_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
];
{
let mut app_mut = app.lock().await;
app_mut.server_tabs.set_index(2);
app_mut.data.lidarr_data.history.sort_asc = true;
}
if use_custom_sorting {
let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
expected_history_items.sort_by(cmp_fn);
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app
.lock()
.await
.data
.lidarr_data
.history
.sorting(vec![history_sort_option]);
}
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetHistory(500))
.await;
mock.assert_async().await;
assert!(result.is_ok());
let LidarrSerdeable::HistoryWrapper(history) = result.unwrap() else {
panic!("Expected LidarrHistoryWrapper")
};
assert_eq!(
app.lock().await.data.lidarr_data.history.items,
expected_history_items
);
assert!(app.lock().await.data.lidarr_data.history.sort_asc);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!({"records": [{
"id": 123,
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]});
let response: LidarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("pageSize=500&sortDirection=descending&sortKey=date")
.build_for(LidarrEvent::GetHistory(500))
.await;
app.lock().await.data.lidarr_data.history.sort_asc = true;
app
.lock()
.await
.push_navigation_stack(ActiveLidarrBlock::HistorySortPrompt.into());
let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app
.lock()
.await
.data
.lidarr_data
.history
.sorting(vec![history_sort_option]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::HistoryWrapper(history) = network
.handle_lidarr_event(LidarrEvent::GetHistory(500))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryWrapper")
};
mock.assert_async().await;
assert!(app.lock().await.data.lidarr_data.history.is_empty());
assert!(app.lock().await.data.lidarr_data.history.sort_asc);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_mark_lidarr_history_item_as_failed_event() {
let history_item_id = 1234i64;
let (mock, app, _server) = MockServarrApi::post()
.returns(json!({}))
.path("/1234")
.build_for(LidarrEvent::MarkHistoryItemAsFailed(history_item_id))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::MarkHistoryItemAsFailed(history_item_id))
.await;
mock.assert_async().await;
assert!(result.is_ok());
}
}
+63
View File
@@ -0,0 +1,63 @@
use crate::models::Route;
use crate::models::lidarr_models::LidarrHistoryWrapper;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use log::info;
use serde_json::Value;
#[cfg(test)]
#[path = "lidarr_history_network_tests.rs"]
mod lidarr_history_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn get_lidarr_history(
&mut self,
events: u64,
) -> Result<LidarrHistoryWrapper> {
info!("Fetching all Lidarr history events");
let event = LidarrEvent::GetHistory(events);
let params = format!("pageSize={events}&sortDirection=descending&sortKey=date");
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
.await;
self
.handle_request::<(), LidarrHistoryWrapper>(request_props, |history_response, mut app| {
if !matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::HistorySortPrompt, _)
) {
let mut history_vec = history_response.records;
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.lidarr_data.history.set_items(history_vec);
app.data.lidarr_data.history.apply_sorting_toggle(false);
}
})
.await
}
pub(in crate::network::lidarr_network) async fn mark_lidarr_history_item_as_failed(
&mut self,
history_item_id: i64,
) -> Result<Value> {
info!("Marking the Lidarr history item with ID: {history_item_id} as 'failed'");
let event = LidarrEvent::MarkHistoryItemAsFailed(history_item_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Post,
None,
Some(format!("/{history_item_id}")),
None,
)
.await;
self
.handle_request::<(), Value>(request_props, |_, _| ())
.await
}
}
@@ -4,10 +4,11 @@ pub mod test_utils {
use crate::models::HorizontallyScrollableText;
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, Member, MetadataProfile,
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, LidarrHistoryData,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, Member, MetadataProfile,
NewItemMonitorType, Ratings, SystemStatus,
};
use crate::models::servarr_models::{QualityProfile, RootFolder, Tag};
use crate::models::servarr_models::{Quality, QualityProfile, QualityWrapper, RootFolder, Tag};
use bimap::BiMap;
use chrono::DateTime;
use serde_json::Number;
@@ -120,6 +121,16 @@ pub mod test_utils {
}
}
pub fn quality_wrapper() -> QualityWrapper {
QualityWrapper { quality: quality() }
}
pub fn quality() -> Quality {
Quality {
name: "Lossless".to_string(),
}
}
pub fn quality_profile() -> QualityProfile {
QualityProfile {
id: 1,
@@ -249,4 +260,31 @@ pub mod test_utils {
statistics: Some(album_statistics()),
}
}
pub fn lidarr_history_wrapper() -> LidarrHistoryWrapper {
LidarrHistoryWrapper {
records: vec![lidarr_history_item()],
}
}
pub fn lidarr_history_item() -> LidarrHistoryItem {
LidarrHistoryItem {
id: 1,
source_title: "Test source title".into(),
album_id: 1,
artist_id: 1,
quality: quality_wrapper(),
date: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
event_type: LidarrHistoryEventType::Grabbed,
data: lidarr_history_data(),
}
}
pub fn lidarr_history_data() -> LidarrHistoryData {
LidarrHistoryData {
dropped_path: Some("/nfs/nzbget/completed/music/Something/cool.mp3".to_owned()),
imported_path: Some("/nfs/music/Something/Album 1/Cool.mp3".to_owned()),
..LidarrHistoryData::default()
}
}
}
@@ -30,6 +30,11 @@ mod tests {
assert_str_eq!(event.resource(), "/artist");
}
#[rstest]
fn test_resource_history(#[values(LidarrEvent::GetHistory(0))] event: LidarrEvent) {
assert_str_eq!(event.resource(), "/history");
}
#[rstest]
fn test_resource_tag(
#[values(
@@ -83,6 +88,7 @@ mod tests {
#[case(LidarrEvent::GetStatus, "/system/status")]
#[case(LidarrEvent::GetTags, "/tag")]
#[case(LidarrEvent::HealthCheck, "/health")]
#[case(LidarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")]
fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) {
assert_str_eq!(event.resource(), expected_uri);
}
+13
View File
@@ -9,6 +9,7 @@ use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
mod downloads;
mod history;
mod library;
mod root_folders;
mod system;
@@ -34,7 +35,9 @@ pub enum LidarrEvent {
GetArtistDetails(i64),
GetDiskSpace,
GetDownloads(u64),
GetHistory(u64),
GetHostConfig,
MarkHistoryItemAsFailed(i64),
GetMetadataProfiles,
GetQualityProfiles,
GetRootFolders,
@@ -67,6 +70,8 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::DeleteAlbum(_) => "/album",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) => "/queue",
LidarrEvent::GetHistory(_) => "/history",
LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::TriggerAutomaticArtistSearch(_)
| LidarrEvent::UpdateAllArtists
@@ -120,6 +125,14 @@ impl Network<'_, '_> {
.get_lidarr_downloads(count)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetHistory(events) => self
.get_lidarr_history(events)
.await
.map(LidarrSerdeable::from),
LidarrEvent::MarkHistoryItemAsFailed(history_item_id) => self
.mark_lidarr_history_item_as_failed(history_item_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetHostConfig => self
.get_lidarr_host_config()
.await