Compare commits

...

2 Commits

27 changed files with 2570 additions and 51 deletions
+59 -2
View File
@@ -6,8 +6,8 @@ use crate::app::context_clues::{
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS,
ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS,
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS,
};
#[cfg(test)]
@@ -92,6 +92,55 @@ pub static MANUAL_ARTIST_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static ALBUM_DETAILS_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
];
pub static ALBUM_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_ALBUM_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub(in crate::app) struct LidarrContextClueProvider;
impl ContextClueProvider for LidarrContextClueProvider {
@@ -106,6 +155,14 @@ impl ContextClueProvider for LidarrContextClueProvider {
.lidarr_data
.artist_info_tabs
.get_active_route_contextual_help(),
_ if ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_details_tabs
.get_active_route_contextual_help(),
ActiveLidarrBlock::AddArtistSearchInput
| ActiveLidarrBlock::AddArtistEmptySearchResults
| ActiveLidarrBlock::TestAllIndexers
+146 -3
View File
@@ -7,14 +7,16 @@ mod tests {
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::lidarr::lidarr_context_clues::{
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES,
ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ALBUM_DETAILS_CONTEXT_CLUES,
ALBUM_HISTORY_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES,
ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS,
INDEXER_SETTINGS_BLOCKS, LidarrData,
};
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use rstest::rstest;
@@ -236,6 +238,123 @@ mod tests {
assert_none!(manual_artist_search_context_clues_iter.next());
}
#[test]
fn test_album_details_context_clues() {
let mut album_details_context_clues_iter = ALBUM_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "episode details")
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete episode")
);
assert_none!(album_details_context_clues_iter.next());
}
#[test]
fn test_album_history_context_clues() {
let mut album_history_context_clues_iter = ALBUM_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(album_history_context_clues_iter.next());
}
#[test]
fn test_manual_album_search_context_clues() {
let mut manual_album_search_context_clues_iter = MANUAL_ALBUM_SEARCH_CONTEXT_CLUES.iter();
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(manual_album_search_context_clues_iter.next());
}
#[rstest]
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::ArtistHistory, &ARTIST_HISTORY_CONTEXT_CLUES)]
@@ -255,6 +374,30 @@ mod tests {
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[rstest]
#[case(0, ActiveLidarrBlock::AlbumDetails, &ALBUM_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::AlbumHistory, &ALBUM_HISTORY_CONTEXT_CLUES)]
#[case(2, ActiveLidarrBlock::ManualAlbumSearch, &MANUAL_ALBUM_SEARCH_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_album_details_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
let mut album_details_modal = AlbumDetailsModal::default();
album_details_modal.album_details_tabs.set_index(index);
let lidarr_data = LidarrData {
album_details_modal: Some(album_details_modal),
..LidarrData::default()
};
app.data.lidarr_data = lidarr_data;
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[test]
fn test_lidarr_context_clue_provider_artists_block() {
let mut app = App::test_default();
@@ -362,7 +505,7 @@ mod tests {
}
#[test]
fn test_sonarr_context_clue_provider_system_tasks_clues() {
fn test_lidarr_context_clue_provider_system_tasks_clues() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into());
+37 -1
View File
@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::models::lidarr_models::{Artist, LidarrRelease};
use crate::models::lidarr_models::{Album, Artist, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_models::Indexer;
use crate::network::NetworkEvent;
@@ -120,6 +120,42 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_album_details_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app.data.lidarr_data.albums.set_items(vec![Album {
id: 1,
..Album::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumDetails)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTracks(1, 1).into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTrackFiles(1).into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_downloads_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
+17
View File
@@ -54,6 +54,19 @@ impl App<'_> {
.await;
}
}
ActiveLidarrBlock::AlbumDetails => {
let artist_id = self.extract_artist_id().await;
let album_id = self.extract_album_id().await;
self
.dispatch_network_event(LidarrEvent::GetTracks(artist_id, album_id).into())
.await;
self
.dispatch_network_event(LidarrEvent::GetTrackFiles(album_id).into())
.await;
self
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
.await;
}
ActiveLidarrBlock::AddArtistSearchResults => {
self
.dispatch_network_event(
@@ -134,6 +147,10 @@ impl App<'_> {
self.data.lidarr_data.artists.current_selection().id
}
async fn extract_album_id(&self) -> i64 {
self.data.lidarr_data.albums.current_selection().id
}
async fn extract_lidarr_indexer_id(&self) -> i64 {
self.data.lidarr_data.indexers.current_selection().id
}
+12
View File
@@ -28,6 +28,11 @@ pub enum LidarrDeleteCommand {
#[arg(long, help = "Add a list exclusion for this album")]
add_list_exclusion: bool,
},
#[command(about = "Delete the specified track file from disk")]
TrackFile {
#[arg(long, help = "The ID of the track file to delete", required = true)]
track_file_id: i64,
},
#[command(about = "Delete an artist from your Lidarr library")]
Artist {
#[arg(long, help = "The ID of the artist to delete", required = true)]
@@ -102,6 +107,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::TrackFile { track_file_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteTrackFile(track_file_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Artist {
artist_id,
delete_files_from_disk,
@@ -86,6 +86,40 @@ mod tests {
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_track_file_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "track-file"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_track_file_success() {
let expected_args = LidarrDeleteCommand::TrackFile { track_file_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"track-file",
"--track-file-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]);
@@ -327,6 +361,32 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_track_file_command() {
let expected_track_file_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteTrackFile(expected_track_file_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_track_file_command = LidarrDeleteCommand::TrackFile { track_file_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_track_file_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_artist_command() {
let expected_delete_artist_params = DeleteParams {
+70
View File
@@ -27,6 +27,23 @@ pub enum LidarrListCommand {
)]
artist_id: i64,
},
#[command(
about = "Fetch all history events for the given album corresponding to the artist with the given ID."
)]
AlbumHistory {
#[arg(
long,
help = "The Lidarr artist ID of the artist whose history you wish to fetch and list",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The Lidarr album ID to fetch history events for",
required = true
)]
album_id: i64,
},
#[command(about = "Fetch all history events for the artist with the given ID")]
ArtistHistory {
#[arg(
@@ -72,6 +89,32 @@ pub enum LidarrListCommand {
Tags,
#[command(about = "List all Lidarr tasks")]
Tasks,
#[command(
about = "List the tracks for the album that corresponds to the artist with the given ID"
)]
Tracks {
#[arg(
long,
help = "The Lidarr artist ID of the artist whose tracks you wish to fetch",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The Lidarr album ID whose tracks you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "List the track files for the album with the given ID")]
TrackFiles {
#[arg(
long,
help = "The Lidarr ID of the album whose track files you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "List all Lidarr updates")]
Updates,
}
@@ -110,6 +153,16 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::AlbumHistory {
artist_id,
album_id,
} => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbumHistory(artist_id, album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::ArtistHistory { artist_id } => {
let resp = self
.network
@@ -204,6 +257,23 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Tracks {
artist_id,
album_id,
} => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTracks(artist_id, album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::TrackFiles { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTrackFiles(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Updates => {
let resp = self
.network
@@ -69,6 +69,58 @@ mod tests {
assert_eq!(album_command, expected_args);
}
#[test]
fn test_album_history_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_history_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_history_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_list_artist_history_requires_artist_id() {
let result =
@@ -172,6 +224,101 @@ mod tests {
};
assert_eq!(logs_command, expected_args);
}
#[test]
fn test_list_tracks_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"tracks",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_tracks_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"tracks",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_tracks_success() {
let expected_args = LidarrListCommand::Tracks {
artist_id: 1,
album_id: 1,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"tracks",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(tracks_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(tracks_command, expected_args);
}
#[test]
fn test_list_track_files_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "track-files"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_files_success() {
let expected_args = LidarrListCommand::TrackFiles { album_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"track-files",
"--album-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(track_files_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(track_files_command, expected_args);
}
}
mod handler {
@@ -248,6 +395,36 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_list_album_history_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumHistory(expected_artist_id, expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_album_history_command = LidarrListCommand::AlbumHistory {
artist_id: 1,
album_id: 1,
};
let result =
LidarrListCommandHandler::with(&app_arc, list_album_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_artist_history_command() {
let expected_artist_id = 1;
@@ -353,5 +530,60 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_tracks_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTracks(expected_artist_id, expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_tracks_command = LidarrListCommand::Tracks {
artist_id: 1,
album_id: 1,
};
let result = LidarrListCommandHandler::with(&app_arc, list_tracks_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_track_files_command() {
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackFiles(expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_track_files_command = LidarrListCommand::TrackFiles { album_id: 1 };
let result =
LidarrListCommandHandler::with(&app_arc, list_track_files_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -17,6 +17,19 @@ mod manual_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrManualSearchCommand {
#[command(
about = "Trigger a manual search of releases for the given album corresponding to the artist with the given ID"
)]
Album {
#[arg(
long,
help = "The Lidarr ID of the artist whose releases you wish to fetch and list",
required = true
)]
artist_id: i64,
#[arg(long, help = "The Lidarr album ID to search for", required = true)]
album_id: i64,
},
#[command(
about = "Trigger a manual search of discography releases for the given artist corresponding to the artist with the given ID."
)]
@@ -59,6 +72,27 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrManualSearchCommand>
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrManualSearchCommand::Album {
artist_id,
album_id,
} => {
println!("Searching for album releases. This may take a minute...");
match self
.network
.handle_network_event(LidarrEvent::GetAlbumReleases(artist_id, album_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => {
let albums_vec: Vec<LidarrRelease> = releases_vec
.into_iter()
.filter(|release| !release.discography)
.collect();
serde_json::to_string_pretty(&albums_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
LidarrManualSearchCommand::Discography { artist_id } => {
println!("Searching for artist discography releases. This may take a minute...");
match self
@@ -23,6 +23,58 @@ mod tests {
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_manual_album_search_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_album_search_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_album_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_manual_discography_search_requires_artist_id() {
let result =
@@ -65,6 +117,39 @@ mod tests {
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_manual_album_search_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumReleases(expected_artist_id, expected_album_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_album_search_command = LidarrManualSearchCommand::Album {
artist_id: 1,
album_id: 1,
};
let result = LidarrManualSearchCommandHandler::with(
&app_arc,
manual_album_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_manual_discography_search_command() {
let expected_artist_id = 1;
@@ -18,6 +18,15 @@ mod trigger_automatic_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrTriggerAutomaticSearchCommand {
#[command(about = "Trigger an automatic search for the album with the specified ID")]
Album {
#[arg(
long,
help = "The Lidarr ID of the album you want to trigger an automatic search for",
required = true
)]
album_id: i64,
},
#[command(about = "Trigger an automatic search for the artist with the specified ID")]
Artist {
#[arg(
@@ -58,6 +67,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrTriggerAutomaticSearchCommand>
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrTriggerAutomaticSearchCommand::Album { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::TriggerAutomaticAlbumSearch(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrTriggerAutomaticSearchCommand::Artist { artist_id } => {
let resp = self
.network
@@ -28,6 +28,36 @@ mod tests {
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_trigger_automatic_album_search_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"album",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_album_search_with_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"album",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_trigger_automatic_artist_search_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
@@ -75,6 +105,35 @@ mod tests {
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_trigger_automatic_album_search_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticAlbumSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrTriggerAutomaticSearchCommand::Album { album_id: 1 };
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_trigger_automatic_artist_search_command() {
let mut mock_network = MockNetworkTrait::new();
@@ -1,17 +1,23 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::Number;
use std::cmp::Ordering;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::handlers::lidarr_handlers::library::artist_details_handler::{
ArtistDetailsHandler, releases_sorting_options,
};
use crate::models::HorizontallyScrollableText;
use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
};
use crate::models::servarr_models::{Quality, QualityWrapper};
mod test_handle_delete {
use super::*;
@@ -50,6 +56,8 @@ mod tests {
use rstest::rstest;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
@@ -59,7 +67,8 @@ mod tests {
fn test_left_right_prompt_toggle(
#[values(
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
ActiveLidarrBlock::AutomaticallySearchArtistPrompt
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
#[values(Key::Left, Key::Right)] key: Key,
@@ -76,6 +85,50 @@ mod tests {
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[rstest]
#[case(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[case(
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
#[case(
ActiveLidarrBlock::ManualArtistSearch,
ActiveLidarrBlock::ArtistDetails
)]
fn test_artist_details_tabs_left_right_action(
#[case] left_block: ActiveLidarrBlock,
#[case] right_block: ActiveLidarrBlock,
#[values(true, false)] is_loading: bool,
) {
let mut app = App::test_default_fully_populated();
app.is_loading = is_loading;
app.push_navigation_stack(right_block.into());
app.data.lidarr_data.artist_info_tabs.index = app
.data
.lidarr_data
.artist_info_tabs
.tabs
.iter()
.position(|tab_route| tab_route.route == right_block.into())
.unwrap_or_default();
ArtistDetailsHandler::new(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle();
assert_eq!(
app.get_current_route(),
app.data.lidarr_data.artist_info_tabs.get_active_route()
);
assert_navigation_pushed!(app, left_block.into());
ArtistDetailsHandler::new(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle();
assert_eq!(
app.get_current_route(),
app.data.lidarr_data.artist_info_tabs.get_active_route()
);
assert_navigation_pushed!(app, right_block.into());
}
}
mod test_handle_submit {
@@ -84,11 +137,14 @@ mod tests {
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrReleaseDownloadBody};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
artist, torrent_release,
};
use crate::{assert_navigation_popped, assert_navigation_pushed};
use pretty_assertions::assert_eq;
use rstest::rstest;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
@@ -181,6 +237,106 @@ mod tests {
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_manual_artist_search_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.discography_releases
.set_items(vec![torrent_release()]);
app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into());
ArtistDetailsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::ManualArtistSearch,
None,
)
.handle();
assert_navigation_pushed!(
app,
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into()
);
}
#[test]
fn test_manual_artist_search_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into());
ArtistDetailsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::ManualArtistSearch,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ManualArtistSearch.into()
);
}
#[test]
fn test_manual_artist_search_confirm_prompt_confirm_submit() {
let mut app = App::test_default();
let release = torrent_release();
app
.data
.lidarr_data
.discography_releases
.set_items(vec![release.clone()]);
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into());
app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into());
ArtistDetailsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_navigation_popped!(app, ActiveLidarrBlock::ManualArtistSearch.into());
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody {
guid: release.guid,
indexer_id: release.indexer_id,
}))
);
}
#[test]
fn test_manual_artist_search_confirm_prompt_decline_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.discography_releases
.set_items(vec![torrent_release()]);
app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into());
app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into());
ArtistDetailsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_navigation_popped!(app, ActiveLidarrBlock::ManualArtistSearch.into());
assert_none!(app.data.lidarr_data.prompt_confirm_action);
}
}
mod test_handle_esc {
@@ -193,6 +349,7 @@ mod tests {
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_eq;
use ratatui::widgets::TableState;
use rstest::rstest;
@@ -200,7 +357,7 @@ mod tests {
#[test]
fn test_artist_history_details_block_esc() {
let mut app = App::test_default();
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into());
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistoryDetails.into());
@@ -242,7 +399,8 @@ mod tests {
fn test_artist_details_esc(
#[values(
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::UpdateAndScanArtistPrompt
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt
)]
prompt_block: ActiveLidarrBlock,
#[values(true, false)] is_ready: bool,
@@ -258,6 +416,31 @@ mod tests {
assert!(!app.data.lidarr_data.prompt_confirm);
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
}
#[rstest]
fn test_artist_details_blocks_esc(
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.data.lidarr_data.artist_history.filter = None;
app.data.lidarr_data.artist_history.filtered_items = None;
app.data.lidarr_data.artist_history.filtered_state = None;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(active_lidarr_block.into());
ArtistDetailsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert_is_empty!(app.data.lidarr_data.albums);
assert_is_empty!(app.data.lidarr_data.discography_releases);
assert_is_empty!(app.data.lidarr_data.artist_history);
assert_eq!(app.data.lidarr_data.artist_info_tabs.index, 0);
}
}
mod test_handle_char_key_event {
@@ -266,18 +449,23 @@ mod tests {
use crate::assert_navigation_pushed;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::lidarr_models::Artist;
use crate::models::lidarr_models::{Artist, LidarrReleaseDownloadBody};
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS,
};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::torrent_release;
use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_artist_details_edit_key(
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
@@ -310,7 +498,11 @@ mod tests {
#[rstest]
fn test_artist_details_edit_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
@@ -403,7 +595,11 @@ mod tests {
#[rstest]
fn test_artist_details_auto_search_key(
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
@@ -425,7 +621,11 @@ mod tests {
#[rstest]
fn test_artist_details_auto_search_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
@@ -445,7 +645,11 @@ mod tests {
#[rstest]
fn test_artist_details_update_key(
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
@@ -464,7 +668,11 @@ mod tests {
#[rstest]
fn test_artist_details_update_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
@@ -484,7 +692,11 @@ mod tests {
#[rstest]
fn test_artist_details_refresh_key(
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
@@ -506,7 +718,11 @@ mod tests {
#[rstest]
fn test_artist_details_refresh_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ManualArtistSearch
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
@@ -560,6 +776,37 @@ mod tests {
&expected_action
);
}
#[test]
fn test_manual_artist_search_confirm_prompt_confirm_key() {
let mut app = App::test_default();
let release = torrent_release();
app
.data
.lidarr_data
.discography_releases
.set_items(vec![release.clone()]);
app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into());
app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_navigation_popped!(app, ActiveLidarrBlock::ManualArtistSearch.into());
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody {
guid: release.guid,
indexer_id: release.indexer_id,
}))
);
}
}
#[test]
@@ -692,4 +939,252 @@ mod tests {
assert!(handler.is_ready());
}
#[test]
fn test_artist_details_handler_is_not_ready_when_not_loading_and_discography_releases_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
let handler = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ManualArtistSearch,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_artist_details_handler_ready_when_not_loading_and_discography_releases_is_non_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app
.data
.lidarr_data
.discography_releases
.set_items(vec![LidarrRelease::default()]);
let handler = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ManualArtistSearch,
None,
);
assert!(handler.is_ready());
}
#[test]
fn test_delegates_delete_album_blocks_to_delete_album_handler() {
let mut app = App::test_default();
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ArtistDetails.into()
);
}
#[test]
fn test_releases_sorting_options_source() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering =
|a, b| a.protocol.cmp(&b.protocol);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[0].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Source");
}
#[test]
fn test_releases_sorting_options_age() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| a.age.cmp(&b.age);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[1].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Age");
}
#[test]
fn test_releases_sorting_options_rejected() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering =
|a, b| a.rejected.cmp(&b.rejected);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[2].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Rejected");
}
#[test]
fn test_releases_sorting_options_title() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| {
a.title
.text
.to_lowercase()
.cmp(&b.title.text.to_lowercase())
};
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[3].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Title");
}
#[test]
fn test_releases_sorting_options_indexer() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering =
|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase());
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[4].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Indexer");
}
#[test]
fn test_releases_sorting_options_size() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering =
|a, b| a.size.cmp(&b.size);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[5].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Size");
}
#[test]
fn test_releases_sorting_options_peers() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| {
let default_number = Number::from(i64::MAX);
let seeder_a = a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
seeder_a.cmp(&seeder_b)
};
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[6].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Peers");
}
#[test]
fn test_releases_sorting_options_quality() {
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering =
|a, b| a.quality.cmp(&b.quality);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[7].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Quality");
}
fn release_vec() -> Vec<LidarrRelease> {
let release_a = LidarrRelease {
protocol: "Protocol A".to_owned(),
age: 1,
title: HorizontallyScrollableText::from("Title A"),
indexer: "Indexer A".to_owned(),
size: 1,
rejected: true,
seeders: Some(Number::from(1)),
quality: QualityWrapper {
quality: Quality {
name: "Quality A".to_owned(),
},
},
..LidarrRelease::default()
};
let release_b = LidarrRelease {
protocol: "Protocol B".to_owned(),
age: 2,
title: HorizontallyScrollableText::from("title B"),
indexer: "indexer B".to_owned(),
size: 2,
rejected: false,
seeders: Some(Number::from(2)),
quality: QualityWrapper {
quality: Quality {
name: "Quality B".to_owned(),
},
},
..LidarrRelease::default()
};
let release_c = LidarrRelease {
protocol: "Protocol C".to_owned(),
age: 3,
title: HorizontallyScrollableText::from("Title C"),
indexer: "Indexer C".to_owned(),
size: 3,
rejected: false,
seeders: None,
quality: QualityWrapper {
quality: Quality {
name: "Quality C".to_owned(),
},
},
..LidarrRelease::default()
};
vec![release_a, release_b, release_c]
}
}
+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);
}
}
@@ -1,11 +1,18 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{Album, DeleteParams, LidarrSerdeable};
use crate::models::lidarr_models::{
Album, DeleteParams, LidarrHistoryItem, LidarrRelease, LidarrSerdeable,
};
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::stateful_table::SortOption;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::ALBUM_JSON;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
ALBUM_JSON, lidarr_history_item, torrent_release,
};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use mockito::Matcher;
use pretty_assertions::assert_eq;
use rstest::rstest;
use serde_json::{Value, json};
#[tokio::test]
@@ -128,4 +135,397 @@ mod tests {
assert_eq!(album, expected_album);
}
#[rstest]
#[tokio::test]
async fn test_handle_get_lidarr_album_history_event(
#[values(true, false)] use_custom_sorting: bool,
) {
let history_json = json!([{
"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: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let mut expected_history_items = vec![
LidarrHistoryItem {
id: 123,
artist_id: 1007,
album_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
];
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());
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()
.album_history
.sorting(vec![history_sort_option]);
}
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_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::GetAlbumHistory(1, 1))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
mock.assert_async().await;
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_history
.items,
expected_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_lidarr_album_history_event_empty_album_details_modal() {
let history_json = json!([{
"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: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let expected_history_items = vec![
LidarrHistoryItem {
id: 123,
artist_id: 1007,
album_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=1&albumId=1")
.build_for(LidarrEvent::GetAlbumHistory(1, 1))
.await;
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;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_history
.items,
expected_history_items
);
assert!(
!app
.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!([
{
"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 {
guid: "4567".to_owned(),
..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&albumId=1")
.build_for(LidarrEvent::GetAlbumReleases(1, 1))
.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::Releases(releases_vec) = network
.handle_lidarr_event(LidarrEvent::GetAlbumReleases(1, 1))
.await
.unwrap()
else {
panic!("Expected Releases")
};
mock.assert_async().await;
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_releases
.items,
vec![expected_filtered_lidarr_release]
);
assert_eq!(releases_vec, expected_raw_lidarr_releases);
}
#[tokio::test]
async fn test_handle_get_album_releases_event_empty_album_details_modal() {
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_lidarr_release = LidarrRelease {
guid: "4567".to_owned(),
..torrent_release()
};
let (mock, app, _server) = MockServarrApi::get()
.returns(release_json)
.query("artistId=1&albumId=1")
.build_for(LidarrEvent::GetAlbumReleases(1, 1))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::GetAlbumReleases(1, 1))
.await
);
mock.assert_async().await;
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_releases
.items,
vec![expected_lidarr_release]
);
}
#[tokio::test]
async fn test_handle_trigger_automatic_album_search_event() {
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "AlbumSearch",
"albumIds": [1]
}))
.returns(json!({}))
.build_for(LidarrEvent::TriggerAutomaticAlbumSearch(1))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::TriggerAutomaticAlbumSearch(1))
.await
.is_ok()
);
mock.assert_async().await;
}
}
@@ -1,4 +1,7 @@
use crate::models::lidarr_models::{Album, DeleteParams};
use crate::models::lidarr_models::{
Album, DeleteParams, LidarrCommandBody, LidarrHistoryItem, LidarrRelease,
};
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
@@ -57,6 +60,88 @@ impl Network<'_, '_> {
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_album_history(
&mut self,
artist_id: i64,
album_id: i64,
) -> Result<Vec<LidarrHistoryItem>> {
let event = LidarrEvent::GetAlbumHistory(artist_id, album_id);
info!("Fetching history for artist with ID: {artist_id} and album with ID: {album_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| {
if app.data.lidarr_data.album_details_modal.is_none() {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
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);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_album_releases(
&mut self,
artist_id: i64,
album_id: i64,
) -> Result<Vec<LidarrRelease>> {
let event = LidarrEvent::GetAlbumReleases(artist_id, album_id);
info!("Fetching releases for artist with ID: {artist_id} and album with ID: {album_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("artistId={artist_id}&albumId={album_id}")),
)
.await;
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_releases_vec = release_vec
.into_iter()
.filter(|release| !release.discography)
.collect();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_releases
.set_items(album_releases_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn delete_album(
&mut self,
delete_album_params: DeleteParams,
@@ -150,4 +235,26 @@ impl Network<'_, '_> {
}
}
}
pub(in crate::network::lidarr_network) async fn trigger_automatic_album_search(
&mut self,
album_id: i64,
) -> Result<Value> {
let event = LidarrEvent::TriggerAutomaticAlbumSearch(album_id);
info!("Searching indexers for album with ID: {album_id}");
let body = LidarrCommandBody {
name: "AlbumSearch".to_owned(),
album_ids: Some(vec![album_id]),
..LidarrCommandBody::default()
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<LidarrCommandBody, Value>(request_props, |_, _| ())
.await
}
}
@@ -7,6 +7,7 @@ use serde_json::Value;
mod albums;
mod artists;
mod tracks;
#[cfg(test)]
#[path = "lidarr_library_network_tests.rs"]
@@ -0,0 +1,173 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{track, track_file};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use pretty_assertions::assert_eq;
use serde_json::json;
#[tokio::test]
async fn test_handle_delete_lidarr_track_file_event() {
let (async_server, app_arc, _server) = MockServarrApi::delete()
.path("/1")
.build_for(LidarrEvent::DeleteTrackFile(1))
.await;
app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
assert!(
network
.handle_lidarr_event(LidarrEvent::DeleteTrackFile(1))
.await
.is_ok()
);
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_tracks_event() {
let expected_tracks = vec![track()];
let (mock, app, _server) = MockServarrApi::get()
.query("artistId=1&albumId=1")
.returns(json!([track()]))
.build_for(LidarrEvent::GetTracks(1, 1))
.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 result = network
.handle_lidarr_event(LidarrEvent::GetTracks(1, 1))
.await;
mock.assert_async().await;
let LidarrSerdeable::Tracks(tracks) = result.unwrap() else {
panic!("Expected Tracks variant")
};
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.tracks
.items,
expected_tracks
);
assert_eq!(tracks, expected_tracks);
}
#[tokio::test]
async fn test_handle_get_tracks_event_empty_album_details_modal() {
let expected_tracks = vec![track()];
let (mock, app, _server) = MockServarrApi::get()
.query("artistId=1&albumId=1")
.returns(json!([track()]))
.build_for(LidarrEvent::GetTracks(1, 1))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::GetTracks(1, 1))
.await;
mock.assert_async().await;
let LidarrSerdeable::Tracks(tracks) = result.unwrap() else {
panic!("Expected Tracks variant")
};
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.tracks
.items,
expected_tracks
);
assert_eq!(tracks, expected_tracks);
}
#[tokio::test]
async fn test_handle_get_track_files_event() {
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(json!([track_file()]))
.query("albumId=1")
.build_for(LidarrEvent::GetTrackFiles(1))
.await;
app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let LidarrSerdeable::TrackFiles(track_files) = network
.handle_lidarr_event(LidarrEvent::GetTrackFiles(1))
.await
.unwrap()
else {
panic!("Expected TrackFiles")
};
async_server.assert_async().await;
assert_eq!(
app_arc
.lock()
.await
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_files
.items,
vec![track_file()]
);
assert_eq!(track_files, vec![track_file()]);
}
#[tokio::test]
async fn test_handle_get_track_files_event_empty_album_details_modal() {
let (async_server, app_arc, _server) = MockServarrApi::get()
.returns(json!([track_file()]))
.query("albumId=1")
.build_for(LidarrEvent::GetTrackFiles(1))
.await;
app_arc.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app_arc);
let LidarrSerdeable::TrackFiles(track_files) = network
.handle_lidarr_event(LidarrEvent::GetTrackFiles(1))
.await
.unwrap()
else {
panic!("Expected TrackFiles")
};
async_server.assert_async().await;
let app = app_arc.lock().await;
assert_some!(&app.data.lidarr_data.album_details_modal);
assert_eq!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_files
.items,
vec![track_file()]
);
assert_eq!(track_files, vec![track_file()]);
}
}
@@ -0,0 +1,106 @@
use crate::models::lidarr_models::{Track, TrackFile};
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use log::info;
#[cfg(test)]
#[path = "lidarr_tracks_network_tests.rs"]
mod lidarr_tracks_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn delete_lidarr_track_file(
&mut self,
track_file_id: i64,
) -> Result<()> {
let event = LidarrEvent::DeleteTrackFile(track_file_id);
info!("Deleting Lidarr track file for track file with id: {track_file_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{track_file_id}")),
None,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn get_tracks(
&mut self,
artist_id: i64,
album_id: i64,
) -> Result<Vec<Track>> {
let event = LidarrEvent::GetTracks(artist_id, album_id);
info!("Fetching tracks for Lidarr artist with ID: {artist_id} and album with ID: {album_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("artistId={artist_id}&albumId={album_id}")),
)
.await;
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() {
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
}
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.tracks
.set_items(track_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_track_files(
&mut self,
album_id: i64,
) -> Result<Vec<TrackFile>> {
let event = LidarrEvent::GetTrackFiles(album_id);
info!("Fetching tracks files for Lidarr album with ID: {album_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("albumId={album_id}")),
)
.await;
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
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_files
.set_items(track_file_vec);
})
.await
}
}
@@ -3,9 +3,10 @@
pub mod test_utils {
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, LidarrHistoryData,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrRelease, LidarrTask,
LidarrTaskName, Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus,
AudioTags, DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams,
LidarrHistoryData, LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper,
LidarrRelease, LidarrTask, LidarrTaskName, MediaInfo, Member, MetadataProfile,
NewItemMonitorType, Ratings, SystemStatus, Track, TrackFile,
};
use crate::models::servarr_models::IndexerSettings;
use crate::models::servarr_models::{
@@ -424,4 +425,54 @@ pub mod test_utils {
quality: quality_wrapper(),
}
}
pub fn audio_tags() -> AudioTags {
AudioTags {
title: "When I Get There".to_string(),
artist_title: "P!nk".to_string(),
album_title: "Trustfall".to_string(),
disc_number: 1,
disc_count: 1,
year: 2023,
duration: "00:03:20.1802267".to_string(),
}
}
pub fn media_info() -> MediaInfo {
MediaInfo {
audio_bitrate: Some("1563 kbps".to_owned()),
audio_channels: 2,
audio_codec: Some("FLAC".to_owned()),
audio_bits: Some("24bit".to_owned()),
audio_sample_rate: Some("44.1kHz".to_owned()),
}
}
pub fn track_file() -> TrackFile {
TrackFile {
id: 1,
path: "/music/P!nk/TRUSTFALL/01 - When I Get There.flac".to_string(),
size: 39216378,
quality: quality_wrapper(),
date_added: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()),
media_info: Some(media_info()),
audio_tags: Some(audio_tags()),
}
}
pub fn track() -> Track {
Track {
id: 1,
artist_id: 1,
foreign_track_id: "test-foreign-track-id".to_string(),
track_file_id: 1,
album_id: 1,
explicit: false,
track_number: "1".to_string(),
title: "Test title".to_string(),
duration: 200173,
has_file: false,
ratings: ratings(),
}
}
}
@@ -61,8 +61,11 @@ mod tests {
}
#[rstest]
fn test_resource_history(#[values(LidarrEvent::GetHistory(0))] event: LidarrEvent) {
assert_str_eq!(event.resource(), "/history");
fn test_resource_artist_history(
#[values(LidarrEvent::GetArtistHistory(0), LidarrEvent::GetAlbumHistory(0, 0))]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/history/artist");
}
#[rstest]
@@ -89,6 +92,7 @@ mod tests {
#[values(
LidarrEvent::UpdateAllArtists,
LidarrEvent::TriggerAutomaticArtistSearch(0),
LidarrEvent::TriggerAutomaticAlbumSearch(0),
LidarrEvent::UpdateAndScanArtist(0),
LidarrEvent::UpdateDownloads,
LidarrEvent::GetQueuedEvents,
@@ -128,13 +132,21 @@ mod tests {
fn test_resource_release(
#[values(
LidarrEvent::GetDiscographyReleases(0),
LidarrEvent::DownloadRelease(Default::default())
LidarrEvent::DownloadRelease(Default::default()),
LidarrEvent::GetAlbumReleases(0, 0)
)]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/release");
}
#[rstest]
fn test_resource_track_file(
#[values(LidarrEvent::DeleteTrackFile(0), LidarrEvent::GetTrackFiles(0))] event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/trackfile");
}
#[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
#[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")]
@@ -146,9 +158,10 @@ mod tests {
#[case(LidarrEvent::GetUpdates, "/update")]
#[case(LidarrEvent::HealthCheck, "/health")]
#[case(LidarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")]
#[case(LidarrEvent::GetArtistHistory(0), "/history/artist")]
#[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);
}
+38 -3
View File
@@ -35,12 +35,15 @@ pub enum LidarrEvent {
DeleteIndexer(i64),
DeleteRootFolder(i64),
DeleteTag(i64),
DeleteTrackFile(i64),
DownloadRelease(LidarrReleaseDownloadBody),
EditArtist(EditArtistParams),
EditAllIndexerSettings(IndexerSettings),
EditIndexer(EditIndexerParams),
GetAlbums(i64),
GetAlbumDetails(i64),
GetAlbumHistory(i64, i64),
GetAlbumReleases(i64, i64),
GetArtistHistory(i64),
GetAllIndexerSettings,
GetArtistDetails(i64),
@@ -58,6 +61,8 @@ pub enum LidarrEvent {
GetRootFolders,
GetSecurityConfig,
GetStatus,
GetTracks(i64, i64),
GetTrackFiles(i64),
GetUpdates,
GetTags,
GetTasks,
@@ -70,6 +75,7 @@ pub enum LidarrEvent {
ToggleAlbumMonitoring(i64),
ToggleArtistMonitoring(i64),
TriggerAutomaticArtistSearch(i64),
TriggerAutomaticAlbumSearch(i64),
UpdateAllArtists,
UpdateAndScanArtist(i64),
UpdateDownloads,
@@ -79,6 +85,7 @@ impl NetworkResource for LidarrEvent {
fn resource(&self) -> &'static str {
match &self {
LidarrEvent::AddTag(_) | LidarrEvent::DeleteTag(_) | LidarrEvent::GetTags => "/tag",
LidarrEvent::DeleteTrackFile(_) | LidarrEvent::GetTrackFiles(_) => "/trackfile",
LidarrEvent::GetAllIndexerSettings | LidarrEvent::EditAllIndexerSettings(_) => {
"/config/indexer"
}
@@ -92,13 +99,15 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::ToggleAlbumMonitoring(_)
| LidarrEvent::GetAlbumDetails(_)
| LidarrEvent::DeleteAlbum(_) => "/album",
LidarrEvent::GetArtistHistory(_) => "/history/artist",
LidarrEvent::GetArtistHistory(_) | LidarrEvent::GetAlbumHistory(_, _) => "/history/artist",
LidarrEvent::GetLogs(_) => "/log",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue",
LidarrEvent::GetHistory(_) => "/history",
LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
LidarrEvent::GetDiscographyReleases(_) | LidarrEvent::DownloadRelease(_) => "/release",
LidarrEvent::GetDiscographyReleases(_)
| LidarrEvent::DownloadRelease(_)
| LidarrEvent::GetAlbumReleases(_, _) => "/release",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::GetIndexers | LidarrEvent::DeleteIndexer(_) | LidarrEvent::EditIndexer(_) => {
"/indexer"
@@ -108,7 +117,8 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::UpdateAndScanArtist(_)
| LidarrEvent::UpdateDownloads
| LidarrEvent::GetQueuedEvents
| LidarrEvent::StartTask(_) => "/command",
| LidarrEvent::StartTask(_)
| LidarrEvent::TriggerAutomaticAlbumSearch(_) => "/command",
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
LidarrEvent::GetQualityProfiles => "/qualityprofile",
LidarrEvent::GetRootFolders
@@ -118,6 +128,7 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::TestAllIndexers => "/indexer/testall",
LidarrEvent::GetStatus => "/system/status",
LidarrEvent::GetTasks => "/system/task",
LidarrEvent::GetTracks(_, _) => "/track",
LidarrEvent::GetUpdates => "/update",
LidarrEvent::HealthCheck => "/health",
LidarrEvent::SearchNewArtist(_) => "/artist/lookup",
@@ -152,6 +163,10 @@ impl Network<'_, '_> {
.delete_lidarr_download(download_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::DeleteTrackFile(track_file_id) => self
.delete_lidarr_track_file(track_file_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::EditAllIndexerSettings(params) => self
.edit_all_lidarr_indexer_settings(params)
.await
@@ -191,6 +206,14 @@ impl Network<'_, '_> {
.get_album_details(album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetAlbumHistory(artist_id, album_id) => self
.get_lidarr_album_history(artist_id, album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetAlbumReleases(artist_id, album_id) => self
.get_album_releases(artist_id, album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetDiscographyReleases(artist_id) => self
.get_artist_discography_releases(artist_id)
.await
@@ -244,6 +267,14 @@ 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::GetTracks(artist_id, album_id) => self
.get_tracks(artist_id, album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetTrackFiles(album_id) => self
.get_track_files(album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetUpdates => self.get_lidarr_updates().await.map(LidarrSerdeable::from),
LidarrEvent::HealthCheck => self
.get_lidarr_healthcheck()
@@ -269,6 +300,10 @@ impl Network<'_, '_> {
.trigger_automatic_artist_search(artist_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::TriggerAutomaticAlbumSearch(album_id) => self
.trigger_automatic_album_search(album_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from),
LidarrEvent::UpdateAndScanArtist(artist_id) => self
.update_and_scan_artist(artist_id)