feat: CLI and TUI support for track history and track details in Lidarr

This commit is contained in:
2026-01-19 14:50:20 -07:00
parent 7add62b245
commit eff1a901eb
54 changed files with 3462 additions and 329 deletions
@@ -17,6 +17,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -30,6 +31,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -49,6 +51,7 @@ mod tests {
id: 123,
album_id: 1007,
artist_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -56,6 +59,7 @@ mod tests {
id: 456,
album_id: 2001,
artist_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -113,6 +117,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -126,6 +131,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -3,6 +3,7 @@ mod tests {
use crate::models::lidarr_models::{
Album, DeleteParams, LidarrHistoryItem, LidarrRelease, LidarrSerdeable,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::stateful_table::SortOption;
use crate::network::lidarr_network::LidarrEvent;
@@ -146,6 +147,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -159,6 +161,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -173,6 +176,7 @@ mod tests {
id: 123,
artist_id: 1007,
album_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -180,6 +184,7 @@ mod tests {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -270,6 +275,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -283,6 +289,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -297,6 +304,7 @@ mod tests {
id: 123,
artist_id: 1007,
album_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -304,6 +312,7 @@ mod tests {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -350,6 +359,95 @@ mod tests {
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_album_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 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,
"trackId": 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: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=1&albumId=1")
.build_for(LidarrEvent::GetAlbumHistory(1, 1))
.await;
app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_history
.sort_asc = true;
app
.lock()
.await
.push_navigation_stack(ActiveLidarrBlock::AlbumHistorySortPrompt.into());
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetAlbumHistory(1, 1))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
assert_is_empty!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_history
.items
);
assert!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_history
.sort_asc
);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_album_releases_event() {
let release_json = json!([
@@ -1,7 +1,8 @@
use crate::models::Route;
use crate::models::lidarr_models::{
Album, DeleteParams, LidarrCommandBody, LidarrHistoryItem, LidarrRelease,
};
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
@@ -75,28 +76,25 @@ impl Network<'_, '_> {
self
.handle_request::<(), Vec<LidarrHistoryItem>>(request_props, |history_items, mut app| {
if app.data.lidarr_data.album_details_modal.is_none() {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
let is_sorting = matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::AlbumHistorySortPrompt, _)
);
let mut history_vec = history_items;
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_history
.set_items(history_vec);
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_history
.apply_sorting_toggle(false);
if !is_sorting {
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.get_or_insert_default();
let mut history_vec = history_items;
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
album_details_modal.album_history.set_items(history_vec);
album_details_modal
.album_history
.apply_sorting_toggle(false);
}
})
.await
}
@@ -121,21 +119,18 @@ impl Network<'_, '_> {
self
.handle_request::<(), Vec<LidarrRelease>>(request_props, |release_vec, mut app| {
if app.data.lidarr_data.album_details_modal.is_none() {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.get_or_insert_default();
let album_releases_vec = release_vec
.into_iter()
.filter(|release| !release.discography)
.collect();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
album_details_modal
.album_releases
.set_items(album_releases_vec);
})
@@ -113,6 +113,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -126,6 +127,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -140,6 +142,7 @@ mod tests {
id: 123,
artist_id: 1007,
album_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -147,6 +150,7 @@ mod tests {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -212,6 +216,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -225,6 +230,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -239,6 +245,7 @@ mod tests {
id: 123,
artist_id: 1007,
album_id: 1007,
track_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
@@ -246,6 +253,7 @@ mod tests {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
@@ -289,6 +297,7 @@ mod tests {
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"trackId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -302,6 +311,7 @@ mod tests {
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"trackId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
@@ -1,11 +1,17 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable, Track, TrackFile};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::{AlbumDetailsModal, TrackDetailsModal};
use crate::models::stateful_table::SortOption;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{track, track_file};
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
lidarr_history_item, track, track_file,
};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use pretty_assertions::assert_eq;
use indoc::formatdoc;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::json;
#[tokio::test]
@@ -28,6 +34,272 @@ mod tests {
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_track_details_event() {
let response = track();
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(track()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
});
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let result = network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await;
async_server.assert_async().await;
assert_ok!(&result);
let LidarrSerdeable::Track(track) = result.unwrap() else {
panic!("Expected Track")
};
assert_eq!(track, response);
let app = app_arc.lock().await;
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
ActiveLidarrBlock::TrackDetails.into()
);
let track_details = &app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details;
assert_str_eq!(
track_details.get_text(),
formatdoc!(
"
Title: Test title
Track Number: 1
Duration: 3:20
Explicit: false
Quality: Lossless
File Path: /music/P!nk/TRUSTFALL/01 - When I Get There.flac
File Size: 37.40 MB
Date Added: 2023-05-20 21:29:16 UTC
Codec: FLAC
Channels: 2
Bits: 24bit
Bit Rate: 1563 kbps
Sample Rate: 44.1kHz
"
)
)
}
#[tokio::test]
async fn test_handle_get_track_details_event_empty_media_info() {
let expected_track = Track {
track_file: Some(TrackFile {
media_info: None,
..track_file()
}),
..track()
};
let response = expected_track.clone();
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(expected_track.clone()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
});
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let result = network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await;
async_server.assert_async().await;
assert_ok!(&result);
let LidarrSerdeable::Track(track) = result.unwrap() else {
panic!("Expected Track")
};
assert_eq!(track, response);
let app = app_arc.lock().await;
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
ActiveLidarrBlock::TrackDetails.into()
);
let track_details = &app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details;
assert_str_eq!(
track_details.get_text(),
formatdoc!(
"
Title: Test title
Track Number: 1
Duration: 3:20
Explicit: false
Quality: Lossless
File Path: /music/P!nk/TRUSTFALL/01 - When I Get There.flac
File Size: 37.40 MB
Date Added: 2023-05-20 21:29:16 UTC
"
)
)
}
#[tokio::test]
async fn test_handle_get_track_details_event_empty_track_file() {
let expected_track = Track {
track_file: None,
..track()
};
let response = expected_track.clone();
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(expected_track.clone()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
});
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let result = network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await;
async_server.assert_async().await;
assert_ok!(&result);
let LidarrSerdeable::Track(track) = result.unwrap() else {
panic!("Expected Track")
};
assert_eq!(track, response);
let app = app_arc.lock().await;
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
ActiveLidarrBlock::TrackDetails.into()
);
let track_details = &app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details;
assert_str_eq!(
track_details.get_text(),
formatdoc!(
"
Title: Test title
Track Number: 1
Duration: 3:20
Explicit: false
"
)
)
}
#[tokio::test]
async fn test_handle_get_track_details_event_album_details_modal_not_required_in_cli_mode() {
let response = track();
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(track()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.cli_mode = true;
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let result = network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await;
async_server.assert_async().await;
assert_ok!(&result);
let LidarrSerdeable::Track(track) = result.unwrap() else {
panic!("Expected Track")
};
assert_eq!(track, response);
let app = app_arc.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_some!(
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
}
#[tokio::test]
#[should_panic(expected = "Album details modal is empty")]
async fn test_handle_get_track_details_event_requires_album_details_modal_to_be_some_when_in_tui_mode()
{
let (_async_server, app_arc, _server) = MockServarrApi::get()
.returns(serde_json::to_value(track()).unwrap())
.path("/1")
.build_for(LidarrEvent::GetTrackDetails(1))
.await;
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
network
.handle_lidarr_event(LidarrEvent::GetTrackDetails(1))
.await
.unwrap();
}
#[tokio::test]
async fn test_handle_get_tracks_event() {
let expected_tracks = vec![track()];
@@ -170,4 +442,430 @@ mod tests {
);
assert_eq!(track_files, vec![track_file()]);
}
#[rstest]
#[tokio::test]
async fn test_handle_get_lidarr_track_history_event(
#[values(true, false)] use_custom_sorting: bool,
) {
let history_json = json!([{
"id": 123,
"sourceTitle": "z track",
"albumId": 1007,
"artistId": 1007,
"trackId": 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": "A Track",
"albumId": 2001,
"artistId": 2001,
"trackId": 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: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let mut expected_history_items = vec![LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "A Track".into(),
..lidarr_history_item()
}];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=2001&albumId=2001")
.build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await;
let album_details_modal = AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
};
app.lock().await.data.lidarr_data.album_details_modal = Some(album_details_modal);
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
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_history
.sorting(vec![history_sort_option]);
}
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_history
.sort_asc = true;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.items,
expected_history_items
);
assert!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.sort_asc
);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_track_history_event_empty_track_details_modal() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z track",
"albumId": 1007,
"artistId": 1007,
"trackId": 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": "A Track",
"albumId": 2001,
"artistId": 2001,
"trackId": 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: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let expected_history_items = vec![LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "A Track".into(),
..lidarr_history_item()
}];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=2001&albumId=2001")
.build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await;
app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_some!(
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.items,
expected_history_items
);
assert!(
!app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.sort_asc
);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_track_history_event_empty_album_details_modal() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z track",
"albumId": 1007,
"artistId": 1007,
"trackId": 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": "A Track",
"albumId": 2001,
"artistId": 2001,
"trackId": 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: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let expected_history_items = vec![LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
track_id: 2001,
source_title: "A Track".into(),
..lidarr_history_item()
}];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=2001&albumId=2001")
.build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_some!(
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.items,
expected_history_items
);
assert!(
!app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.sort_asc
);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_track_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z track",
"albumId": 1007,
"artistId": 1007,
"trackId": 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": "A Track",
"albumId": 2001,
"artistId": 2001,
"trackId": 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: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=2001&albumId=2001")
.build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await;
let album_details_modal = AlbumDetailsModal {
track_details_modal: Some(TrackDetailsModal::default()),
..AlbumDetailsModal::default()
};
app.lock().await.data.lidarr_data.album_details_modal = Some(album_details_modal);
app.lock().await.server_tabs.set_index(2);
app
.lock()
.await
.push_navigation_stack(ActiveLidarrBlock::TrackHistorySortPrompt.into());
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history) = network
.handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_some!(
&app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
assert_is_empty!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.items,
);
assert!(
!app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_history
.sort_asc
);
assert_eq!(history, response);
}
}
+159 -15
View File
@@ -1,8 +1,11 @@
use crate::models::lidarr_models::{Track, TrackFile};
use crate::models::lidarr_models::{LidarrHistoryItem, MediaInfo, Track, TrackFile};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::{Route, ScrollableText};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use indoc::formatdoc;
use log::info;
#[cfg(test)]
@@ -53,18 +56,117 @@ impl Network<'_, '_> {
self
.handle_request::<(), Vec<Track>>(request_props, |mut track_vec, mut app| {
track_vec.sort_by(|a, b| a.id.cmp(&b.id));
if app.data.lidarr_data.album_details_modal.is_none() {
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.get_or_insert_default();
album_details_modal.tracks.set_items(track_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_track_details(
&mut self,
track_id: i64,
) -> Result<Track> {
let event = LidarrEvent::GetTrackDetails(track_id);
info!("Fetching Lidarr track details for track with ID: {track_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
Some(format!("/{track_id}")),
None,
)
.await;
self
.handle_request::<(), Track>(request_props, |track_response, mut app| {
if app.cli_mode {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
app
let Track {
explicit,
track_number,
title,
duration,
track_file,
..
} = track_response;
let duration_secs = duration / 1000;
let mins = duration_secs / 60;
let secs = duration_secs % 60;
let track_length = format!("{mins}:{secs:02}");
let track_details_modal = app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.tracks
.set_items(track_vec);
.expect("Album details modal is empty")
.track_details_modal
.get_or_insert_default();
let mut details = formatdoc!(
"
Title: {title}
Track Number: {track_number}
Duration: {track_length}
Explicit: {explicit}
"
);
if let Some(file) = track_file {
let TrackFile {
path,
size,
quality,
date_added,
media_info,
..
} = file;
let quality_name = quality.quality.name;
let size_mb = size as f64 / 1024f64.powi(2);
details.push_str(&formatdoc!(
"
Quality: {quality_name}
File Path: {path}
File Size: {size_mb:.2} MB
Date Added: {date_added}
"
));
if let Some(info) = media_info {
let MediaInfo {
audio_bit_rate,
audio_channels,
audio_codec,
audio_bits,
audio_sample_rate,
} = info;
details.push_str(&formatdoc!(
"
Codec: {}
Channels: {}
Bits: {}
Bit Rate: {}
Sample Rate: {}
",
audio_codec.unwrap_or_default(),
audio_channels,
audio_bits.unwrap_or_default(),
audio_bit_rate.unwrap_or_default(),
audio_sample_rate.unwrap_or_default()
));
}
}
track_details_modal.track_details = ScrollableText::with_string(details);
})
.await
}
@@ -88,18 +190,60 @@ impl Network<'_, '_> {
self
.handle_request::<(), Vec<TrackFile>>(request_props, |track_file_vec, mut app| {
if app.data.lidarr_data.album_details_modal.is_none() {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
app
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_files
.set_items(track_file_vec);
.get_or_insert_default();
album_details_modal.track_files.set_items(track_file_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_track_history(
&mut self,
artist_id: i64,
album_id: i64,
track_id: i64,
) -> Result<Vec<LidarrHistoryItem>> {
let event = LidarrEvent::GetTrackHistory(artist_id, album_id, track_id);
info!(
"Fetching history for artist with ID: {artist_id} and album with ID: {album_id} and track with ID: {track_id}"
);
let params = format!("artistId={artist_id}&albumId={album_id}");
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
.await;
self
.handle_request::<(), Vec<LidarrHistoryItem>>(request_props, |history_items, mut app| {
let is_sorting = matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::TrackHistorySortPrompt, _)
);
let album_details_modal = app
.data
.lidarr_data
.album_details_modal
.get_or_insert_default();
let track_details_modal = album_details_modal
.track_details_modal
.get_or_insert_default();
if !is_sorting {
let mut history_vec: Vec<LidarrHistoryItem> = history_items
.into_iter()
.filter(|it| it.track_id == track_id)
.collect();
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
track_details_modal.track_history.set_items(history_vec);
track_details_modal
.track_history
.apply_sorting_toggle(false);
}
})
.await
}
@@ -278,6 +278,7 @@ pub mod test_utils {
source_title: "Test source title".into(),
album_id: 1,
artist_id: 1,
track_id: 1,
quality: quality_wrapper(),
date: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
event_type: LidarrHistoryEventType::Grabbed,
@@ -473,6 +474,7 @@ pub mod test_utils {
duration: 200173,
has_file: false,
ratings: ratings(),
track_file: Some(track_file()),
}
}
}
@@ -62,7 +62,11 @@ mod tests {
#[rstest]
fn test_resource_artist_history(
#[values(LidarrEvent::GetArtistHistory(0), LidarrEvent::GetAlbumHistory(0, 0))]
#[values(
LidarrEvent::GetArtistHistory(0),
LidarrEvent::GetAlbumHistory(0, 0),
LidarrEvent::GetTrackHistory(0, 0, 0)
)]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/history/artist");
@@ -147,6 +151,13 @@ mod tests {
assert_str_eq!(event.resource(), "/trackfile");
}
#[rstest]
fn test_resource_track(
#[values(LidarrEvent::GetTracks(0, 0), LidarrEvent::GetTrackDetails(0))] event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/track");
}
#[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
#[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")]
@@ -161,7 +172,6 @@ mod tests {
#[case(LidarrEvent::GetHistory(0), "/history")]
#[case(LidarrEvent::TestIndexer(0), "/indexer/test")]
#[case(LidarrEvent::TestAllIndexers, "/indexer/testall")]
#[case(LidarrEvent::GetTracks(0, 0), "/track")]
fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) {
assert_str_eq!(event.resource(), expected_uri);
}
+14 -2
View File
@@ -61,8 +61,10 @@ pub enum LidarrEvent {
GetRootFolders,
GetSecurityConfig,
GetStatus,
GetTrackDetails(i64),
GetTracks(i64, i64),
GetTrackFiles(i64),
GetTrackHistory(i64, i64, i64),
GetUpdates,
GetTags,
GetTasks,
@@ -99,7 +101,9 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::ToggleAlbumMonitoring(_)
| LidarrEvent::GetAlbumDetails(_)
| LidarrEvent::DeleteAlbum(_) => "/album",
LidarrEvent::GetArtistHistory(_) | LidarrEvent::GetAlbumHistory(_, _) => "/history/artist",
LidarrEvent::GetArtistHistory(_)
| LidarrEvent::GetAlbumHistory(_, _)
| LidarrEvent::GetTrackHistory(_, _, _) => "/history/artist",
LidarrEvent::GetLogs(_) => "/log",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue",
@@ -128,7 +132,7 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::TestAllIndexers => "/indexer/testall",
LidarrEvent::GetStatus => "/system/status",
LidarrEvent::GetTasks => "/system/task",
LidarrEvent::GetTracks(_, _) => "/track",
LidarrEvent::GetTracks(_, _) | LidarrEvent::GetTrackDetails(_) => "/track",
LidarrEvent::GetUpdates => "/update",
LidarrEvent::HealthCheck => "/health",
LidarrEvent::SearchNewArtist(_) => "/artist/lookup",
@@ -267,6 +271,10 @@ impl Network<'_, '_> {
LidarrEvent::GetStatus => self.get_lidarr_status().await.map(LidarrSerdeable::from),
LidarrEvent::GetTags => self.get_lidarr_tags().await.map(LidarrSerdeable::from),
LidarrEvent::GetTasks => self.get_lidarr_tasks().await.map(LidarrSerdeable::from),
LidarrEvent::GetTrackDetails(track_id) => self
.get_track_details(track_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetTracks(artist_id, album_id) => self
.get_tracks(artist_id, album_id)
.await
@@ -275,6 +283,10 @@ impl Network<'_, '_> {
.get_track_files(album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetTrackHistory(artist_id, album_id, track_id) => self
.get_lidarr_track_history(artist_id, album_id, track_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetUpdates => self.get_lidarr_updates().await.map(LidarrSerdeable::from),
LidarrEvent::HealthCheck => self
.get_lidarr_healthcheck()