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
+16
View File
@@ -44,6 +44,15 @@ pub enum LidarrGetCommand {
SecurityConfig,
#[command(about = "Get the system status")]
SystemStatus,
#[command(about = "Get detailed information for the track with the given ID")]
TrackDetails {
#[arg(
long,
help = "The Lidarr ID of the track whose details you wish to fetch",
required = true
)]
track_id: i64,
},
}
impl From<LidarrGetCommand> for Command {
@@ -115,6 +124,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHan
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::TrackDetails { track_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTrackDetails(track_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
@@ -106,6 +106,32 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_track_details_requires_track_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "track-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_track_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"get",
"track-details",
"--track-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
@@ -273,5 +299,31 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_track_details_command() {
let expected_track_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackDetails(expected_track_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_track_details_command = LidarrGetCommand::TrackDetails { track_id: 1 };
let result =
LidarrGetCommandHandler::with(&app_arc, get_track_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
+46 -2
View File
@@ -2,16 +2,18 @@ use std::sync::Arc;
use anyhow::Result;
use clap::{Subcommand, arg};
use serde_json::json;
use tokio::sync::Mutex;
use super::LidarrCommand;
use crate::models::Serdeable;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "list_command_handler_tests.rs"]
mod list_command_handler_tests;
@@ -89,6 +91,27 @@ pub enum LidarrListCommand {
Tags,
#[command(about = "List all Lidarr tasks")]
Tasks,
#[command(about = "Fetch all history events for the track with the given ID")]
TrackHistory {
#[arg(
long,
help = "The artist ID that the track belongs to",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The album ID that the track is a part of",
required = true
)]
album_id: i64,
#[arg(
long,
help = "The Lidarr ID of the track whose history you wish to fetch",
required = true
)]
track_id: i64,
},
#[command(
about = "List the tracks for the album that corresponds to the artist with the given ID"
)]
@@ -257,6 +280,27 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::TrackHistory {
artist_id,
album_id,
track_id,
} => {
match self
.network
.handle_network_event(LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(history_vec))) => {
let history_items_vec: Vec<LidarrHistoryItem> = history_vec
.into_iter()
.filter(|it| it.track_id == track_id)
.collect();
serde_json::to_string_pretty(&history_items_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
LidarrListCommand::Tracks {
artist_id,
album_id,
+136 -1
View File
@@ -225,6 +225,96 @@ mod tests {
assert_eq!(logs_command, expected_args);
}
#[test]
fn test_list_track_history_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--album-id",
"1",
"--track-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--track-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_requires_track_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_success() {
let expected_args = LidarrListCommand::TrackHistory {
artist_id: 1,
album_id: 1,
track_id: 1,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--album-id",
"1",
"--track-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(track_history_command))) =
result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(track_history_command, expected_args);
}
#[test]
fn test_list_tracks_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
@@ -325,6 +415,7 @@ mod tests {
use std::sync::Arc;
use mockall::predicate::eq;
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
@@ -332,8 +423,9 @@ mod tests {
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::lidarr_history_item;
use crate::{
app::App,
network::{MockNetworkTrait, NetworkEvent},
@@ -531,6 +623,49 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_track_history_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let expected_track_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackHistory(expected_artist_id, expected_album_id, expected_track_id)
.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(
vec![
lidarr_history_item(),
LidarrHistoryItem {
track_id: 2,
..lidarr_history_item()
},
],
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_track_history_command = LidarrListCommand::TrackHistory {
artist_id: expected_artist_id,
album_id: expected_album_id,
track_id: expected_track_id,
};
let result =
LidarrListCommandHandler::with(&app_arc, list_track_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&[lidarr_history_item()]).unwrap()
);
}
#[tokio::test]
async fn test_handle_list_tracks_command() {
let expected_artist_id = 1;
@@ -109,16 +109,20 @@ mod tests {
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
torrent_release, usenet_release,
};
use crate::network::{MockNetworkTrait, NetworkEvent};
use mockall::predicate::eq;
use serde_json::json;
use pretty_assertions::assert_str_eq;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_manual_album_search_command() {
let expected_releases = [torrent_release()];
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
@@ -129,9 +133,13 @@ mod tests {
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
torrent_release(),
LidarrRelease {
discography: true,
..usenet_release()
},
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_album_search_command = LidarrManualSearchCommand::Album {
@@ -148,10 +156,18 @@ mod tests {
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&expected_releases).unwrap()
);
}
#[tokio::test]
async fn test_manual_discography_search_command() {
let expected_releases = [LidarrRelease {
discography: true,
..usenet_release()
}];
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
@@ -161,9 +177,13 @@ mod tests {
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
torrent_release(),
LidarrRelease {
discography: true,
..usenet_release()
},
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_discography_search_command =
@@ -178,6 +198,10 @@ mod tests {
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&expected_releases).unwrap()
);
}
}
}