feat: CLI support for searching for discography releases in Lidarr

This commit is contained in:
2026-01-15 11:39:34 -07:00
parent d7f0dd5950
commit 8dfa664a06
13 changed files with 445 additions and 10 deletions
+30 -1
View File
@@ -136,7 +136,7 @@ mod tests {
#[test] #[test]
fn test_start_task_requires_task_name() { fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "start-task"]); let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "start-task"]);
assert_err!(&result); assert_err!(&result);
assert_eq!( assert_eq!(
@@ -232,6 +232,7 @@ mod tests {
use crate::cli::lidarr::add_command_handler::LidarrAddCommand; use crate::cli::lidarr::add_command_handler::LidarrAddCommand;
use crate::cli::lidarr::edit_command_handler::LidarrEditCommand; use crate::cli::lidarr::edit_command_handler::LidarrEditCommand;
use crate::cli::lidarr::get_command_handler::LidarrGetCommand; use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand; use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand; use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
use crate::models::lidarr_models::LidarrTaskName; use crate::models::lidarr_models::LidarrTaskName;
@@ -436,6 +437,34 @@ mod tests {
assert_ok!(&result); assert_ok!(&result);
} }
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler()
{
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command =
LidarrCommand::ManualSearch(LidarrManualSearchCommand::Discography { artist_id: 1 });
let result =
LidarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test] #[tokio::test]
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler() async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{ {
@@ -0,0 +1,71 @@
use crate::app::App;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::{CliCommandHandler, Command};
use crate::network::NetworkTrait;
use crate::network::lidarr_network::LidarrEvent;
use anyhow::Result;
use clap::Subcommand;
use std::sync::Arc;
use tokio::sync::Mutex;
#[cfg(test)]
#[path = "manual_search_command_handler_tests.rs"]
mod manual_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrManualSearchCommand {
#[command(
about = "Trigger a manual search of discography releases for the given artist corresponding to the artist with the given ID.\nNote that when downloading a discography release, ensure that the release includes 'discography: true', otherwise you'll run into issues"
)]
Discography {
#[arg(
long,
help = "The Lidarr ID of the artist whose discography releases you wish to fetch and list",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrManualSearchCommand> for Command {
fn from(value: LidarrManualSearchCommand) -> Self {
Command::Lidarr(LidarrCommand::ManualSearch(value))
}
}
pub(super) struct LidarrManualSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrManualSearchCommand>
for LidarrManualSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrManualSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrManualSearchCommand::Discography { artist_id } => {
println!("Searching for artist discography releases. This may take a minute...");
let resp = self
.network
.handle_network_event(LidarrEvent::GetDiscographyReleases(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,98 @@
#[cfg(test)]
mod tests {
use crate::cli::Command;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_manual_search_command_from() {
let command = LidarrManualSearchCommand::Discography { artist_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Lidarr(LidarrCommand::ManualSearch(command))
);
}
mod cli {
use crate::Cli;
use clap::CommandFactory;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_manual_discography_search_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "manual-search", "discography"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_discography_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"discography",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use crate::app::App;
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::manual_search_command_handler::{
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{MockNetworkTrait, NetworkEvent};
use mockall::predicate::eq;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_manual_discography_search_command() {
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_discography_search_command =
LidarrManualSearchCommand::Discography { artist_id: 1 };
let result = LidarrManualSearchCommandHandler::with(
&app_arc,
manual_discography_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
+11
View File
@@ -15,6 +15,9 @@ use trigger_automatic_search_command_handler::{
}; };
use super::{CliCommandHandler, Command}; use super::{CliCommandHandler, Command};
use crate::cli::lidarr::manual_search_command_handler::{
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::lidarr_models::LidarrTaskName; use crate::models::lidarr_models::LidarrTaskName;
use crate::network::lidarr_network::LidarrEvent; use crate::network::lidarr_network::LidarrEvent;
use crate::{app::App, network::NetworkTrait}; use crate::{app::App, network::NetworkTrait};
@@ -30,6 +33,7 @@ mod trigger_automatic_search_command_handler;
#[cfg(test)] #[cfg(test)]
#[path = "lidarr_command_tests.rs"] #[path = "lidarr_command_tests.rs"]
mod lidarr_command_tests; mod lidarr_command_tests;
mod manual_search_command_handler;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] #[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrCommand { pub enum LidarrCommand {
@@ -63,6 +67,8 @@ pub enum LidarrCommand {
about = "Commands to refresh the data in your Lidarr instance" about = "Commands to refresh the data in your Lidarr instance"
)] )]
Refresh(LidarrRefreshCommand), Refresh(LidarrRefreshCommand),
#[command(subcommand, about = "Commands to manually search for releases")]
ManualSearch(LidarrManualSearchCommand),
#[command( #[command(
subcommand, subcommand,
about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance" about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance"
@@ -186,6 +192,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
.handle() .handle()
.await? .await?
} }
LidarrCommand::ManualSearch(manual_search_command) => {
LidarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network)
.handle()
.await?
}
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => { LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
LidarrTriggerAutomaticSearchCommandHandler::with( LidarrTriggerAutomaticSearchCommandHandler::with(
self.app, self.app,
+25
View File
@@ -464,6 +464,30 @@ impl Display for LidarrTaskName {
} }
} }
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct LidarrRelease {
pub guid: String,
pub protocol: String,
#[serde(deserialize_with = "super::from_i64")]
pub age: i64,
pub title: HorizontallyScrollableText,
pub discography: bool,
pub artist_name: Option<String>,
pub album_title: Option<String>,
pub indexer: String,
#[serde(deserialize_with = "super::from_i64")]
pub indexer_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub size: i64,
pub rejected: bool,
pub rejections: Option<Vec<String>>,
pub seeders: Option<Number>,
pub leechers: Option<Number>,
pub quality: QualityWrapper,
}
impl From<LidarrSerdeable> for Serdeable { impl From<LidarrSerdeable> for Serdeable {
fn from(value: LidarrSerdeable) -> Serdeable { fn from(value: LidarrSerdeable) -> Serdeable {
Serdeable::Lidarr(value) Serdeable::Lidarr(value)
@@ -489,6 +513,7 @@ serde_enum_from!(
MetadataProfiles(Vec<MetadataProfile>), MetadataProfiles(Vec<MetadataProfile>),
QualityProfiles(Vec<QualityProfile>), QualityProfiles(Vec<QualityProfile>),
QueueEvents(Vec<QueueEvent>), QueueEvents(Vec<QueueEvent>),
Releases(Vec<LidarrRelease>),
RootFolders(Vec<RootFolder>), RootFolders(Vec<RootFolder>),
SecurityConfig(SecurityConfig), SecurityConfig(SecurityConfig),
SystemStatus(SystemStatus), SystemStatus(SystemStatus),
+14 -2
View File
@@ -6,8 +6,8 @@ mod tests {
use crate::models::lidarr_models::{ use crate::models::lidarr_models::{
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse, AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, Member, LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrRelease, LidarrTask,
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus, Member, MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
}; };
use crate::models::servarr_models::{ use crate::models::servarr_models::{
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse, DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
@@ -460,6 +460,18 @@ mod tests {
assert_eq!(lidarr_serdeable, LidarrSerdeable::RootFolders(root_folders)); assert_eq!(lidarr_serdeable, LidarrSerdeable::RootFolders(root_folders));
} }
#[test]
fn test_lidarr_serdeable_from_releases() {
let releases = vec![LidarrRelease {
guid: "test".to_owned(),
..LidarrRelease::default()
}];
let lidarr_serdeable: LidarrSerdeable = releases.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Releases(releases));
}
#[test] #[test]
fn test_lidarr_serdeable_from_security_config() { fn test_lidarr_serdeable_from_security_config() {
let security_config = SecurityConfig { let security_config = SecurityConfig {
+13 -1
View File
@@ -8,7 +8,7 @@ use crate::app::context_clues::{
use crate::app::lidarr::lidarr_context_clues::{ use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
}; };
use crate::models::lidarr_models::LidarrTask; use crate::models::lidarr_models::{LidarrRelease, LidarrTask};
use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::{IndexerSettings, QueueEvent}; use crate::models::servarr_models::{IndexerSettings, QueueEvent};
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
@@ -35,6 +35,9 @@ use {
metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map, 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::lidarr_network::lidarr_network_test_utils::test_utils::{log_line, task},
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
torrent_release, usenet_release,
},
crate::network::servarr_test_utils::diskspace, crate::network::servarr_test_utils::diskspace,
crate::network::servarr_test_utils::indexer_test_result, crate::network::servarr_test_utils::indexer_test_result,
crate::network::servarr_test_utils::queued_event, crate::network::servarr_test_utils::queued_event,
@@ -58,6 +61,7 @@ pub struct LidarrData<'a> {
pub artist_info_tabs: TabState, pub artist_info_tabs: TabState,
pub artists: StatefulTable<Artist>, pub artists: StatefulTable<Artist>,
pub delete_files: bool, pub delete_files: bool,
pub discography_releases: StatefulTable<LidarrRelease>,
pub disk_space_vec: Vec<DiskSpace>, pub disk_space_vec: Vec<DiskSpace>,
pub downloads: StatefulTable<DownloadRecord>, pub downloads: StatefulTable<DownloadRecord>,
pub edit_artist_modal: Option<EditArtistModal>, pub edit_artist_modal: Option<EditArtistModal>,
@@ -92,6 +96,7 @@ impl LidarrData<'_> {
pub fn reset_artist_info_tabs(&mut self) { pub fn reset_artist_info_tabs(&mut self) {
self.albums = StatefulTable::default(); self.albums = StatefulTable::default();
self.discography_releases = StatefulTable::default();
self.artist_history = None; self.artist_history = None;
self.artist_info_tabs.index = 0; self.artist_info_tabs.index = 0;
} }
@@ -140,6 +145,7 @@ impl<'a> Default for LidarrData<'a> {
artist_history: None, artist_history: None,
artists: StatefulTable::default(), artists: StatefulTable::default(),
delete_files: false, delete_files: false,
discography_releases: StatefulTable::default(),
disk_space_vec: Vec::new(), disk_space_vec: Vec::new(),
downloads: StatefulTable::default(), downloads: StatefulTable::default(),
edit_artist_modal: None, edit_artist_modal: None,
@@ -333,6 +339,12 @@ impl LidarrData<'_> {
}]); }]);
lidarr_data.history.search = Some("test search".into()); lidarr_data.history.search = Some("test search".into());
lidarr_data.history.filter = Some("test filter".into()); lidarr_data.history.filter = Some("test filter".into());
lidarr_data
.discography_releases
.set_items(vec![torrent_release(), usenet_release()]);
lidarr_data
.discography_releases
.sorting(vec![sort_option!(indexer_id)]);
lidarr_data.root_folders.set_items(vec![root_folder()]); lidarr_data.root_folders.set_items(vec![root_folder()]);
lidarr_data.indexers.set_items(vec![indexer()]); lidarr_data.indexers.set_items(vec![indexer()]);
lidarr_data.queued_events.set_items(vec![queued_event()]); lidarr_data.queued_events.set_items(vec![queued_event()]);
@@ -7,7 +7,7 @@ mod tests {
use crate::app::lidarr::lidarr_context_clues::{ use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
}; };
use crate::models::lidarr_models::Album; use crate::models::lidarr_models::{Album, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::{ use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ARTIST_DETAILS_BLOCKS, 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_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS,
@@ -60,12 +60,16 @@ mod tests {
fn test_reset_artist_info_tabs() { fn test_reset_artist_info_tabs() {
let mut lidarr_data = LidarrData::default(); let mut lidarr_data = LidarrData::default();
lidarr_data.albums.set_items(vec![Album::default()]); lidarr_data.albums.set_items(vec![Album::default()]);
lidarr_data
.discography_releases
.set_items(vec![LidarrRelease::default()]);
lidarr_data.artist_history = Some(StatefulTable::default()); lidarr_data.artist_history = Some(StatefulTable::default());
lidarr_data.artist_info_tabs.index = 1; lidarr_data.artist_info_tabs.index = 1;
lidarr_data.reset_artist_info_tabs(); lidarr_data.reset_artist_info_tabs();
assert_is_empty!(lidarr_data.albums); assert_is_empty!(lidarr_data.albums);
assert_is_empty!(lidarr_data.discography_releases);
assert_none!(lidarr_data.artist_history); assert_none!(lidarr_data.artist_history);
assert_eq!(lidarr_data.artist_info_tabs.index, 0); assert_eq!(lidarr_data.artist_info_tabs.index, 0);
} }
@@ -146,6 +150,7 @@ mod tests {
assert_is_empty!(lidarr_data.downloads); assert_is_empty!(lidarr_data.downloads);
assert_none!(lidarr_data.edit_artist_modal); assert_none!(lidarr_data.edit_artist_modal);
assert_none!(lidarr_data.add_root_folder_modal); assert_none!(lidarr_data.add_root_folder_modal);
assert_is_empty!(lidarr_data.discography_releases);
assert_is_empty!(lidarr_data.history); assert_is_empty!(lidarr_data.history);
assert_is_empty!(lidarr_data.logs); assert_is_empty!(lidarr_data.logs);
assert_is_empty!(lidarr_data.log_details); assert_is_empty!(lidarr_data.log_details);
@@ -2,14 +2,14 @@
mod tests { mod tests {
use crate::models::lidarr_models::{ use crate::models::lidarr_models::{
AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams,
LidarrHistoryItem, LidarrSerdeable, MonitorType, NewItemMonitorType, LidarrHistoryItem, LidarrRelease, LidarrSerdeable, MonitorType, NewItemMonitorType,
}; };
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::{SortOption, StatefulTable}; use crate::models::stateful_table::{SortOption, StatefulTable};
use crate::network::NetworkResource; use crate::network::NetworkResource;
use crate::network::lidarr_network::LidarrEvent; use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON, artist, lidarr_history_item, ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON, artist, lidarr_history_item, torrent_release,
}; };
use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use bimap::BiMap; use bimap::BiMap;
@@ -393,6 +393,87 @@ mod tests {
assert_eq!(history_items, response); assert_eq!(history_items, response);
} }
#[tokio::test]
async fn test_handle_get_artist_discography_releases_event() {
let release_json = json!([
{
"guid": "1234",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"artistName": "Alex",
"albumTitle": "Something",
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"quality": { "quality": { "name": "Lossless" }},
"discography": true
},
{
"guid": "4567",
"protocol": "torrent",
"age": 1,
"title": "Test Release",
"indexer": "kickass torrents",
"indexerId": 2,
"artistName": "Alex",
"albumTitle": "Something",
"size": 1234,
"rejected": true,
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
"seeders": 2,
"leechers": 1,
"quality": { "quality": { "name": "Lossless" }},
}
]);
let expected_filtered_lidarr_release = LidarrRelease {
discography: true,
..torrent_release()
};
let expected_raw_lidarr_releases = vec![
LidarrRelease {
discography: true,
..torrent_release()
},
LidarrRelease {
guid: "4567".to_owned(),
..torrent_release()
},
];
let (mock, app, _server) = MockServarrApi::get()
.returns(release_json)
.query("artistId=1")
.build_for(LidarrEvent::GetDiscographyReleases(1))
.await;
app
.lock()
.await
.data
.lidarr_data
.artists
.set_items(vec![artist()]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::Releases(releases_vec) = network
.handle_lidarr_event(LidarrEvent::GetDiscographyReleases(1))
.await
.unwrap()
else {
panic!("Expected Releases")
};
mock.assert_async().await;
assert_eq!(
app.lock().await.data.lidarr_data.discography_releases.items,
vec![expected_filtered_lidarr_release]
);
assert_eq!(releases_vec, expected_raw_lidarr_releases);
}
#[tokio::test] #[tokio::test]
async fn test_handle_toggle_artist_monitoring_event() { async fn test_handle_toggle_artist_monitoring_event() {
let artist_json = json!({ let artist_json = json!({
@@ -5,7 +5,7 @@ use serde_json::{Value, json};
use crate::models::Route; use crate::models::Route;
use crate::models::lidarr_models::{ use crate::models::lidarr_models::{
AddArtistBody, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, LidarrCommandBody, AddArtistBody, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, LidarrCommandBody,
LidarrHistoryItem, LidarrHistoryItem, LidarrRelease,
}; };
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
@@ -317,6 +317,39 @@ impl Network<'_, '_> {
.await .await
} }
pub(in crate::network::lidarr_network) async fn get_artist_discography_releases(
&mut self,
artist_id: i64,
) -> Result<Vec<LidarrRelease>> {
let event = LidarrEvent::GetDiscographyReleases(artist_id);
info!("Fetching discography releases for artist with ID: {artist_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("artistId={artist_id}")),
)
.await;
self
.handle_request::<(), Vec<LidarrRelease>>(request_props, |release_vec, mut app| {
let artist_releases_vec = release_vec
.into_iter()
.filter(|release| release.discography)
.collect();
app
.data
.lidarr_data
.discography_releases
.set_items(artist_releases_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn edit_artist( pub(in crate::network::lidarr_network) async fn edit_artist(
&mut self, &mut self,
mut edit_artist_params: EditArtistParams, mut edit_artist_params: EditArtistParams,
@@ -4,8 +4,8 @@ pub mod test_utils {
use crate::models::lidarr_models::{ use crate::models::lidarr_models::{
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus, AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, LidarrHistoryData, DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, LidarrHistoryData,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, LidarrTaskName, LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrRelease, LidarrTask,
Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus, LidarrTaskName, Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus,
}; };
use crate::models::servarr_models::IndexerSettings; use crate::models::servarr_models::IndexerSettings;
use crate::models::servarr_models::{ use crate::models::servarr_models::{
@@ -377,4 +377,51 @@ pub mod test_utils {
* Fixed bug 2" * Fixed bug 2"
)) ))
} }
pub fn rejections() -> Vec<String> {
vec![
"Unknown quality profile".to_owned(),
"Release is already mapped".to_owned(),
]
}
pub fn torrent_release() -> LidarrRelease {
LidarrRelease {
guid: "1234".to_owned(),
protocol: "torrent".to_owned(),
age: 1,
title: HorizontallyScrollableText::from("Test Release"),
discography: false,
artist_name: Some("Alex".to_owned()),
album_title: Some("Something".to_owned()),
indexer: "kickass torrents".to_owned(),
indexer_id: 2,
size: 1234,
rejected: true,
rejections: Some(rejections()),
seeders: Some(Number::from(2)),
leechers: Some(Number::from(1)),
quality: quality_wrapper(),
}
}
pub fn usenet_release() -> LidarrRelease {
LidarrRelease {
guid: "1234".to_owned(),
protocol: "usenet".to_owned(),
age: 1,
title: HorizontallyScrollableText::from("Test Release"),
discography: false,
artist_name: Some("Alex".to_owned()),
album_title: Some("Something".to_owned()),
indexer: "DrunkenSlug".to_owned(),
indexer_id: 1,
size: 1234,
rejected: true,
rejections: Some(rejections()),
seeders: None,
leechers: None,
quality: quality_wrapper(),
}
}
} }
@@ -124,6 +124,11 @@ mod tests {
assert_str_eq!(event.resource(), "/rootfolder"); assert_str_eq!(event.resource(), "/rootfolder");
} }
#[rstest]
fn test_resource_release(#[values(LidarrEvent::GetDiscographyReleases(0))] event: LidarrEvent) {
assert_str_eq!(event.resource(), "/release");
}
#[rstest] #[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")] #[case(LidarrEvent::GetDiskSpace, "/diskspace")]
#[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")] #[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")]
+6
View File
@@ -43,6 +43,7 @@ pub enum LidarrEvent {
GetArtistHistory(i64), GetArtistHistory(i64),
GetAllIndexerSettings, GetAllIndexerSettings,
GetArtistDetails(i64), GetArtistDetails(i64),
GetDiscographyReleases(i64),
GetDiskSpace, GetDiskSpace,
GetDownloads(u64), GetDownloads(u64),
GetHistory(u64), GetHistory(u64),
@@ -96,6 +97,7 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue", LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue",
LidarrEvent::GetHistory(_) => "/history", LidarrEvent::GetHistory(_) => "/history",
LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
LidarrEvent::GetDiscographyReleases(_) => "/release",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host", LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::GetIndexers | LidarrEvent::DeleteIndexer(_) | LidarrEvent::EditIndexer(_) => { LidarrEvent::GetIndexers | LidarrEvent::DeleteIndexer(_) | LidarrEvent::EditIndexer(_) => {
"/indexer" "/indexer"
@@ -184,6 +186,10 @@ impl Network<'_, '_> {
.get_album_details(album_id) .get_album_details(album_id)
.await .await
.map(LidarrSerdeable::from), .map(LidarrSerdeable::from),
LidarrEvent::GetDiscographyReleases(artist_id) => self
.get_artist_discography_releases(artist_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from), LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from),
LidarrEvent::GetDownloads(count) => self LidarrEvent::GetDownloads(count) => self
.get_lidarr_downloads(count) .get_lidarr_downloads(count)