Compare commits
34 Commits
tachyonfx
...
f31810e48a
| Author | SHA1 | Date | |
|---|---|---|---|
| f31810e48a | |||
| 09bee7473f | |||
| b2814371f0 | |||
| 269057867f | |||
| 450fdd7106 | |||
| c624d1b9e4 | |||
| e94f78dc7b | |||
| b1a6db21f1 | |||
| ca208ff5e4 | |||
| 1a43d1ec7c | |||
| 4abf705cb5 | |||
| cf98b10d77 | |||
| f0ed71b436 | |||
| 243de47cae | |||
| d3947d9e15 | |||
| 64d8c65831 | |||
| 60c4cf1098 | |||
| 9cc3ccb419 | |||
| 45c61369c8 | |||
| a8609e08c5 | |||
| a18b047f4f | |||
| b1afdaf541 | |||
| 3c1634d1e3 | |||
| 9b4eda6a9d | |||
| 96308afeee | |||
| 4e13d5d34d | |||
| b4a99d1665 | |||
| a012f6ecd5 | |||
| 5afee1998b | |||
| 059fa48bd9 | |||
| 6771a0ab38 | |||
| bc3aeefa6e | |||
| e61537942b | |||
| 5d09b2402c |
Generated
+1037
-397
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -42,7 +42,7 @@ strum = { version = "0.26.3", features = ["derive"] }
|
||||
strum_macros = "0.26.4"
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
tokio-util = "0.7.8"
|
||||
ratatui = { version = "0.29.0", features = [
|
||||
ratatui = { version = "0.30.0", features = [
|
||||
"all-widgets",
|
||||
"unstable-widget-ref",
|
||||
] }
|
||||
@@ -59,7 +59,7 @@ ctrlc = "3.4.5"
|
||||
colored = "3.0.0"
|
||||
async-trait = "0.1.83"
|
||||
dirs-next = "2.0.0"
|
||||
managarr-tree-widget = "0.24.0"
|
||||
managarr-tree-widget = "0.25.0"
|
||||
indicatif = "0.17.9"
|
||||
derive_setters = "0.1.6"
|
||||
deunicode = "1.6.0"
|
||||
|
||||
@@ -8,6 +8,7 @@ mod tests {
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||
@@ -35,6 +36,7 @@ mod tests {
|
||||
theme: None,
|
||||
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
||||
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
||||
lidarr: None,
|
||||
};
|
||||
let expected_tab_routes = vec![
|
||||
TabRoute {
|
||||
@@ -184,6 +186,7 @@ mod tests {
|
||||
..SonarrData::default()
|
||||
};
|
||||
let data = Data {
|
||||
lidarr_data: LidarrData::default(),
|
||||
radarr_data,
|
||||
sonarr_data,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
|
||||
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
|
||||
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
|
||||
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
|
||||
use crate::models::Route;
|
||||
@@ -21,6 +22,7 @@ impl ContextClueProvider for ServarrContextClueProvider {
|
||||
match app.get_current_route() {
|
||||
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
|
||||
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
|
||||
Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_context_clues_tests.rs"]
|
||||
mod lidarr_context_clues_tests;
|
||||
|
||||
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 10] = [
|
||||
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
|
||||
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.update, "update all"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete album"),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
"toggle album monitoring",
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub(in crate::app) struct LidarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for LidarrContextClueProvider {
|
||||
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
|
||||
let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() else {
|
||||
panic!("LidarrContextClueProvider::get_context_clues called with non-Lidarr route");
|
||||
};
|
||||
|
||||
match active_lidarr_block {
|
||||
_ if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_info_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults => {
|
||||
Some(&BARE_POPUP_CONTEXT_CLUES)
|
||||
}
|
||||
_ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistPrompt
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitor
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
|
||||
| ActiveLidarrBlock::AddArtistSelectQualityProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectRootFolder
|
||||
| ActiveLidarrBlock::AddArtistTagsInput
|
||||
| ActiveLidarrBlock::AddArtistAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
|
||||
_ if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
|
||||
Some(&ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES)
|
||||
}
|
||||
_ => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.main_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
|
||||
LidarrContextClueProvider,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, LidarrData,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_artists_context_clues() {
|
||||
let mut artists_context_clues_iter = ARTISTS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, "update all")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
|
||||
);
|
||||
assert_none!(artists_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_context_clues() {
|
||||
let mut artist_details_context_clues_iter = ARTIST_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete album")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
"toggle album monitoring",
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(artist_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_search_results_context_clues() {
|
||||
let mut add_artist_search_results_context_clues_iter =
|
||||
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
add_artist_search_results_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
add_artist_search_results_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "edit search")
|
||||
);
|
||||
assert_none!(add_artist_search_results_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "LidarrContextClueProvider::get_context_clues called with non-Lidarr route"
|
||||
)]
|
||||
fn test_lidarr_context_clue_provider_get_context_clues_non_lidarr_route() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
|
||||
fn test_lidarr_context_clue_provider_artist_info_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data = LidarrData::default();
|
||||
app.data.lidarr_data.artist_info_tabs.set_index(index);
|
||||
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();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artists_sort_prompt_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistsSortPrompt.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_search_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::SearchArtists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_filter_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::FilterArtists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_context_clue_provider_bare_popup_context_clues(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AddArtistSearchInput,
|
||||
ActiveLidarrBlock::AddArtistEmptySearchResults
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() {
|
||||
for active_lidarr_block in EDIT_ARTIST_BLOCKS {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_add_artist_search_results_context_clues() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_add_artist_blocks(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AddArtistPrompt,
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor,
|
||||
ActiveLidarrBlock::AddArtistSelectMonitorNewItems,
|
||||
ActiveLidarrBlock::AddArtistSelectQualityProfile,
|
||||
ActiveLidarrBlock::AddArtistSelectMetadataProfile,
|
||||
ActiveLidarrBlock::AddArtistSelectRootFolder,
|
||||
ActiveLidarrBlock::AddArtistTagsInput,
|
||||
ActiveLidarrBlock::AddArtistAlreadyInLibrary
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::network::NetworkEvent;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_artists() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Artists)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into());
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_artist_details() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into());
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.add_artist_search = Some("test artist".into());
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AddArtistSearchResults)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::SearchNewArtist("test artist".to_owned()).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_add_new_artist_search_query() {
|
||||
let app = App::test_default_fully_populated();
|
||||
|
||||
let query = app.extract_add_new_artist_search_query().await;
|
||||
|
||||
assert_str_eq!(query, "Test Artist");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_artist_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
|
||||
assert_eq!(app.extract_artist_id().await, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use crate::{
|
||||
models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
|
||||
use super::App;
|
||||
|
||||
pub mod lidarr_context_clues;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_tests.rs"]
|
||||
mod lidarr_tests;
|
||||
|
||||
impl App<'_> {
|
||||
pub(super) async fn dispatch_by_lidarr_block(&mut self, active_lidarr_block: &ActiveLidarrBlock) {
|
||||
match active_lidarr_block {
|
||||
ActiveLidarrBlock::Artists => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::ListArtists.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::ArtistDetails => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistSearchResults => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::SearchNewArtist(self.extract_add_new_artist_search_query().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.check_for_lidarr_prompt_action().await;
|
||||
self.reset_tick_count();
|
||||
}
|
||||
|
||||
async fn extract_add_new_artist_search_query(&self) -> String {
|
||||
self
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_search
|
||||
.as_ref()
|
||||
.expect("add_artist_search should be set")
|
||||
.text
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn extract_artist_id(&self) -> i64 {
|
||||
self.data.lidarr_data.artists.current_selection().id
|
||||
}
|
||||
|
||||
async fn check_for_lidarr_prompt_action(&mut self) {
|
||||
if self.data.lidarr_data.prompt_confirm {
|
||||
self.data.lidarr_data.prompt_confirm = false;
|
||||
if let Some(lidarr_event) = self.data.lidarr_data.prompt_confirm_action.take() {
|
||||
self.dispatch_network_event(lidarr_event.into()).await;
|
||||
self.should_refresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn lidarr_on_tick(&mut self, active_lidarr_block: ActiveLidarrBlock) {
|
||||
if self.is_first_render {
|
||||
self.refresh_lidarr_metadata().await;
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
self.is_first_render = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if self.should_refresh {
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
self.refresh_lidarr_metadata().await;
|
||||
}
|
||||
|
||||
if self.is_routing {
|
||||
if !self.should_refresh {
|
||||
self.cancellation_token.cancel();
|
||||
} else {
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
}
|
||||
}
|
||||
|
||||
if self.tick_count.is_multiple_of(self.tick_until_poll) {
|
||||
self.refresh_lidarr_metadata().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_lidarr_metadata(&mut self) {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDiskSpace.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetStatus.into())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
+53
-1
@@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use veil::Redact;
|
||||
|
||||
use crate::cli::Command;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
@@ -25,6 +26,7 @@ mod app_tests;
|
||||
pub mod context_clues;
|
||||
pub mod key_binding;
|
||||
mod key_binding_tests;
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
@@ -96,6 +98,26 @@ impl App<'_> {
|
||||
server_tabs.extend(sonarr_tabs);
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = config.lidarr {
|
||||
let mut unnamed_idx = 0;
|
||||
let lidarr_tabs = lidarr_configs.into_iter().map(|lidarr_config| {
|
||||
let name = if let Some(name) = lidarr_config.name.clone() {
|
||||
name
|
||||
} else {
|
||||
unnamed_idx += 1;
|
||||
format!("Lidarr {unnamed_idx}")
|
||||
};
|
||||
|
||||
TabRoute {
|
||||
title: name,
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(lidarr_config),
|
||||
}
|
||||
});
|
||||
server_tabs.extend(lidarr_tabs);
|
||||
}
|
||||
|
||||
let weight_sorted_tabs = server_tabs
|
||||
.into_iter()
|
||||
.sorted_by(|tab1, tab2| {
|
||||
@@ -176,6 +198,7 @@ impl App<'_> {
|
||||
match self.get_current_route() {
|
||||
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
|
||||
Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
|
||||
Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -264,6 +287,12 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Lidarr".to_owned(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
@@ -272,6 +301,7 @@ impl App<'_> {
|
||||
pub fn test_default_fully_populated() -> Self {
|
||||
App {
|
||||
data: Data {
|
||||
lidarr_data: LidarrData::test_default_fully_populated(),
|
||||
radarr_data: RadarrData::test_default_fully_populated(),
|
||||
sonarr_data: SonarrData::test_default_fully_populated(),
|
||||
},
|
||||
@@ -288,6 +318,12 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Lidarr".to_owned(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
@@ -296,6 +332,7 @@ impl App<'_> {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Data<'a> {
|
||||
pub lidarr_data: LidarrData<'a>,
|
||||
pub radarr_data: RadarrData<'a>,
|
||||
pub sonarr_data: SonarrData<'a>,
|
||||
}
|
||||
@@ -303,13 +340,14 @@ pub struct Data<'a> {
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub theme: Option<String>,
|
||||
pub lidarr: Option<Vec<ServarrConfig>>,
|
||||
pub radarr: Option<Vec<ServarrConfig>>,
|
||||
pub sonarr: Option<Vec<ServarrConfig>>,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn validate(&self) {
|
||||
if self.radarr.is_none() && self.sonarr.is_none() {
|
||||
if self.lidarr.is_none() && self.radarr.is_none() && self.sonarr.is_none() {
|
||||
log_and_print_error(
|
||||
"No Servarr configuration provided in the specified configuration file".to_owned(),
|
||||
);
|
||||
@@ -323,6 +361,10 @@ impl AppConfig {
|
||||
if let Some(sonarr_configs) = &self.sonarr {
|
||||
sonarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = &self.lidarr {
|
||||
lidarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_config_present_for_cli(&self, command: &Command) {
|
||||
@@ -340,6 +382,10 @@ impl AppConfig {
|
||||
msg("Sonarr");
|
||||
process::exit(1);
|
||||
}
|
||||
Command::Lidarr(_) if self.lidarr.is_none() => {
|
||||
msg("Lidarr");
|
||||
process::exit(1);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -356,6 +402,12 @@ impl AppConfig {
|
||||
sonarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = self.lidarr.as_mut() {
|
||||
for lidarr_config in lidarr_configs {
|
||||
lidarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::radarr::ActiveRadarrBlock;
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease,
|
||||
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, MinimumAvailability, Movie,
|
||||
MovieMonitor, RadarrRelease,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||
use crate::models::servarr_models::Indexer;
|
||||
@@ -88,13 +89,13 @@ mod tests {
|
||||
tmdb_id: 1234,
|
||||
title: "Test".to_owned(),
|
||||
root_folder_path: "/nfs2".to_owned(),
|
||||
minimum_availability: "announced".to_owned(),
|
||||
minimum_availability: MinimumAvailability::Announced,
|
||||
monitored: true,
|
||||
quality_profile_id: 2222,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
add_options: AddMovieOptions {
|
||||
monitor: "movieOnly".to_owned(),
|
||||
monitor: MovieMonitor::MovieOnly,
|
||||
search_for_movie: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -455,7 +455,6 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
// This should panic because the route is not a Sonarr route
|
||||
SonarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,13 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_subcommand_delegates_to_lidarr() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completions_requires_argument() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||
@@ -174,4 +181,6 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
// TODO: Implement test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand, arg};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::LidarrCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::lidarr_models::{AddArtistBody, AddArtistOptions, MonitorType, NewItemMonitorType},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "add_command_handler_tests.rs"]
|
||||
mod add_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrAddCommand {
|
||||
#[command(about = "Add a new artist to your Lidarr library")]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The MusicBrainz foreign artist ID of the artist you wish to add to your library",
|
||||
required = true
|
||||
)]
|
||||
foreign_artist_id: String,
|
||||
#[arg(long, help = "The name of the artist", required = true)]
|
||||
artist_name: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all artist data and metadata should live",
|
||||
required = true
|
||||
)]
|
||||
root_folder_path: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the quality profile to use for this artist",
|
||||
required = true
|
||||
)]
|
||||
quality_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the metadata profile to use for this artist",
|
||||
required = true
|
||||
)]
|
||||
metadata_profile_id: i64,
|
||||
#[arg(long, help = "Disable monitoring for this artist")]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag the artist with",
|
||||
value_parser,
|
||||
action = ArgAction::Append
|
||||
)]
|
||||
tag: Vec<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "What Lidarr should monitor for this artist",
|
||||
value_enum,
|
||||
default_value_t = MonitorType::default()
|
||||
)]
|
||||
monitor: MonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "How Lidarr should monitor new items for this artist",
|
||||
value_enum,
|
||||
default_value_t = NewItemMonitorType::default()
|
||||
)]
|
||||
monitor_new_items: NewItemMonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tell Lidarr to not start a search for missing albums once the artist is added to your library"
|
||||
)]
|
||||
no_search_for_missing_albums: bool,
|
||||
},
|
||||
#[command(about = "Add new tag")]
|
||||
Tag {
|
||||
#[arg(long, help = "The name of the tag to be added", required = true)]
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrAddCommand> for Command {
|
||||
fn from(value: LidarrAddCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Add(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrAddCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrAddCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrAddCommand::Artist {
|
||||
foreign_artist_id,
|
||||
artist_name,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
disable_monitoring,
|
||||
tag: tags,
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
no_search_for_missing_albums,
|
||||
} => {
|
||||
let body = AddArtistBody {
|
||||
foreign_artist_id,
|
||||
artist_name,
|
||||
monitored: !disable_monitoring,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
tags,
|
||||
tag_input_string: None,
|
||||
add_options: AddArtistOptions {
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
search_for_missing_albums: !no_search_for_missing_albums,
|
||||
},
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::AddArtist(body).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrAddCommand::Tag { name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::AddTag(name).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, add_command_handler::LidarrAddCommand},
|
||||
},
|
||||
models::lidarr_models::{MonitorType, NewItemMonitorType},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_add_command_from() {
|
||||
let command = LidarrAddCommand::Tag {
|
||||
name: String::new(),
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Add(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "tag"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_success() {
|
||||
let expected_args = LidarrAddCommand::Tag {
|
||||
name: "test".to_owned(),
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "lidarr", "add", "tag", "--name", "test"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_foreign_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--artist-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_artist_name() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_root_folder_path() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_quality_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_metadata_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_success_with_required_args_only() {
|
||||
let expected_args = LidarrAddCommand::Artist {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
disable_monitoring: false,
|
||||
tag: vec![],
|
||||
monitor: MonitorType::default(),
|
||||
monitor_new_items: NewItemMonitorType::default(),
|
||||
no_search_for_missing_albums: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_success_with_all_args() {
|
||||
let expected_args = LidarrAddCommand::Artist {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 2,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::Future,
|
||||
monitor_new_items: NewItemMonitorType::New,
|
||||
no_search_for_missing_albums: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--disable-monitoring",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
"--monitor",
|
||||
"future",
|
||||
"--monitor-new-items",
|
||||
"new",
|
||||
"--no-search-for-missing-albums",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_monitor_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--monitor",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_new_item_monitor_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--monitor-new-items",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_tags_is_repeatable() {
|
||||
let expected_args = LidarrAddCommand::Artist {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 2,
|
||||
disable_monitoring: false,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::default(),
|
||||
monitor_new_items: NewItemMonitorType::default(),
|
||||
no_search_for_missing_albums: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::lidarr::add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::{
|
||||
AddArtistBody, AddArtistOptions, LidarrSerdeable, MonitorType, NewItemMonitorType,
|
||||
};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_tag_command() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_tag_command = LidarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
};
|
||||
|
||||
let result = LidarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_artist_command() {
|
||||
let expected_body = AddArtistBody {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
monitored: false,
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
add_options: AddArtistOptions {
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
search_for_missing_albums: false,
|
||||
},
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::AddArtist(expected_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_artist_command = LidarrAddCommand::Artist {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
no_search_for_missing_albums: true,
|
||||
};
|
||||
|
||||
let result = LidarrAddCommandHandler::with(&app_arc, add_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::lidarr_models::DeleteParams,
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_command_handler_tests.rs"]
|
||||
mod delete_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrDeleteCommand {
|
||||
#[command(about = "Delete an album from your Lidarr library")]
|
||||
Album {
|
||||
#[arg(long, help = "The ID of the album to delete", required = true)]
|
||||
album_id: i64,
|
||||
#[arg(long, help = "Delete the album files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this album")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete an artist from your Lidarr library")]
|
||||
Artist {
|
||||
#[arg(long, help = "The ID of the artist to delete", required = true)]
|
||||
artist_id: i64,
|
||||
#[arg(long, help = "Delete the artist files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this artist")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the tag with the specified ID")]
|
||||
Tag {
|
||||
#[arg(long, help = "The ID of the tag to delete", required = true)]
|
||||
tag_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrDeleteCommand> for Command {
|
||||
fn from(value: LidarrDeleteCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Delete(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrDeleteCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrDeleteCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrDeleteCommand::Album {
|
||||
album_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_album_params = DeleteParams {
|
||||
id: album_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteAlbum(delete_album_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::Artist {
|
||||
artist_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_artist_params = DeleteParams {
|
||||
id: artist_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteArtist(delete_artist_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::Tag { tag_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteTag(tag_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, delete_command_handler::LidarrDeleteCommand},
|
||||
},
|
||||
};
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_delete_command_from() {
|
||||
let command = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Delete(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "album"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_defaults() {
|
||||
let expected_args = LidarrDeleteCommand::Album {
|
||||
album_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "lidarr", "delete", "album", "--album-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_album_all_args_defined() {
|
||||
let expected_args = LidarrDeleteCommand::Album {
|
||||
album_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"album",
|
||||
"--album-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
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"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_defaults() {
|
||||
let expected_args = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "lidarr", "delete", "artist", "--artist-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_all_args_defined() {
|
||||
let expected_args = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
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_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_success() {
|
||||
let expected_args = LidarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "lidarr", "delete", "tag", "--tag-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);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
lidarr::delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler},
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{DeleteParams, LidarrSerdeable},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_album_command() {
|
||||
let expected_delete_album_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteAlbum(expected_delete_album_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_album_command = LidarrDeleteCommand::Album {
|
||||
album_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_album_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_artist_command() {
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteArtist(expected_delete_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_artist_command = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_tag_command() {
|
||||
let expected_tag_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteTag(expected_tag_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_tag_command = LidarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, ArgGroup, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command, mutex_flags_or_option},
|
||||
models::lidarr_models::{EditArtistParams, NewItemMonitorType},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_command_handler_tests.rs"]
|
||||
mod edit_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrEditCommand {
|
||||
#[command(
|
||||
about = "Edit preferences for the specified artist",
|
||||
group(
|
||||
ArgGroup::new("edit_artist")
|
||||
.args([
|
||||
"enable_monitoring",
|
||||
"disable_monitoring",
|
||||
"monitor_new_items",
|
||||
"quality_profile_id",
|
||||
"metadata_profile_id",
|
||||
"root_folder_path",
|
||||
"tag",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the artist whose settings you want to edit",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Enable monitoring of this artist in Lidarr so Lidarr will automatically download releases from this artist if they are available",
|
||||
conflicts_with = "disable_monitoring"
|
||||
)]
|
||||
enable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable monitoring of this artist so Lidarr does not automatically download releases from this artist if they are available",
|
||||
conflicts_with = "enable_monitoring"
|
||||
)]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "How Lidarr should monitor new albums from this artist",
|
||||
value_enum
|
||||
)]
|
||||
monitor_new_items: Option<NewItemMonitorType>,
|
||||
#[arg(long, help = "The ID of the quality profile to use for this artist")]
|
||||
quality_profile_id: Option<i64>,
|
||||
#[arg(long, help = "The ID of the metadata profile to use for this artist")]
|
||||
metadata_profile_id: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all artist data and metadata should live"
|
||||
)]
|
||||
root_folder_path: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag this artist with",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(long, help = "Clear all tags on this artist", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrEditCommand> for Command {
|
||||
fn from(value: LidarrEditCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Edit(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrEditCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrEditCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrEditCommand::Artist {
|
||||
artist_id,
|
||||
enable_monitoring,
|
||||
disable_monitoring,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tag,
|
||||
clear_tags,
|
||||
} => {
|
||||
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
|
||||
let edit_artist_params = EditArtistParams {
|
||||
artist_id,
|
||||
monitored: monitored_value,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tags: tag,
|
||||
tag_input_string: None,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::EditArtist(edit_artist_params).into())
|
||||
.await?;
|
||||
"Artist Updated".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, edit_command_handler::LidarrEditCommand},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_edit_command_from() {
|
||||
let command = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: None,
|
||||
quality_profile_id: None,
|
||||
metadata_profile_id: None,
|
||||
root_folder_path: None,
|
||||
tag: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Edit(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use crate::{Cli, models::lidarr_models::NewItemMonitorType};
|
||||
|
||||
use super::*;
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_with_artist_id_still_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_monitoring_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--enable-monitoring",
|
||||
"--disable-monitoring",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_tag_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--clear-tags",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_artist_assert_argument_flags_require_args(
|
||||
#[values(
|
||||
"--monitor-new-items",
|
||||
"--quality-profile-id",
|
||||
"--metadata-profile-id",
|
||||
"--root-folder-path",
|
||||
"--tag"
|
||||
)]
|
||||
flag: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
flag,
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_monitor_new_items_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--monitor-new-items",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_only_requires_at_least_one_argument_plus_artist_id() {
|
||||
let expected_args = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: None,
|
||||
quality_profile_id: None,
|
||||
metadata_profile_id: None,
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_tag_argument_is_repeatable() {
|
||||
let expected_args = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: None,
|
||||
quality_profile_id: None,
|
||||
metadata_profile_id: None,
|
||||
root_folder_path: None,
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_all_arguments_defined() {
|
||||
let expected_args = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: true,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: Some(NewItemMonitorType::New),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--enable-monitoring",
|
||||
"--monitor-new-items",
|
||||
"new",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
lidarr::edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler},
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{EditArtistParams, LidarrSerdeable, NewItemMonitorType},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_artist_command() {
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 1,
|
||||
monitored: Some(true),
|
||||
monitor_new_items: Some(NewItemMonitorType::New),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
tag_input_string: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_artist_command = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: true,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: Some(NewItemMonitorType::New),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_artist_command_handles_disable_monitoring_flag_properly() {
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 1,
|
||||
monitored: Some(false),
|
||||
monitor_new_items: Some(NewItemMonitorType::None),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
tag_input_string: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_artist_command = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: true,
|
||||
monitor_new_items: Some(NewItemMonitorType::None),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_artist_command_no_monitoring_boolean_flags_returns_none_value() {
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 1,
|
||||
monitored: None,
|
||||
monitor_new_items: Some(NewItemMonitorType::All),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
tag_input_string: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_artist_command = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: Some(NewItemMonitorType::All),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "get_command_handler_tests.rs"]
|
||||
mod get_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrGetCommand {
|
||||
#[command(about = "Get detailed information for the album with the given ID")]
|
||||
AlbumDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the album whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(about = "Get detailed information for the artist with the given ID")]
|
||||
ArtistDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch the host config for your Lidarr instance")]
|
||||
HostConfig,
|
||||
#[command(about = "Fetch the security config for your Lidarr instance")]
|
||||
SecurityConfig,
|
||||
#[command(about = "Get the system status")]
|
||||
SystemStatus,
|
||||
}
|
||||
|
||||
impl From<LidarrGetCommand> for Command {
|
||||
fn from(value: LidarrGetCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Get(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrGetCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrGetCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrGetCommand::AlbumDetails { album_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAlbumDetails(album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::ArtistDetails { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetArtistDetails(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::HostConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetHostConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::SecurityConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetSecurityConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::SystemStatus => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetStatus.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, get_command_handler::LidarrGetCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_get_command_from() {
|
||||
let command = LidarrGetCommand::SystemStatus;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Get(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_album_details_requires_album_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "album-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"get",
|
||||
"album-details",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_requires_artist_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "artist-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"get",
|
||||
"artist-details",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_host_config_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "host-config"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_config_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "security-config"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_status_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "system-status"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
lidarr::get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler},
|
||||
},
|
||||
models::{Serdeable, lidarr_models::LidarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_album_details_command() {
|
||||
let expected_album_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetAlbumDetails(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 get_album_details_command = LidarrGetCommand::AlbumDetails { album_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_album_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_artist_details_command() {
|
||||
let expected_artist_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetArtistDetails(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 get_artist_details_command = LidarrGetCommand::ArtistDetails { artist_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_artist_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_host_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetHostConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_host_config_command = LidarrGetCommand::HostConfig;
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_security_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetSecurityConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_security_config_command = LidarrGetCommand::SecurityConfig;
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_system_status_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_system_status_command = LidarrGetCommand::SystemStatus;
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, list_command_handler::LidarrListCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_command_from() {
|
||||
let command = LidarrCommand::List(LidarrListCommand::Artists);
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(command));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_list_artists_has_no_arg_requirements() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_list_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_add_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_delete_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_artist_monitoring_requires_artist_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-artist-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_artist_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"toggle-artist-monitoring",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_album_monitoring_requires_album_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-album-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_album_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"toggle-album-monitoring",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_artist_requires_query() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "search-new-artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_artist_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"search-new-artist",
|
||||
"--query",
|
||||
"test query",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::lidarr::add_command_handler::LidarrAddCommand;
|
||||
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
|
||||
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
|
||||
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
lidarr::{
|
||||
LidarrCliHandler, LidarrCommand, delete_command_handler::LidarrDeleteCommand,
|
||||
list_command_handler::LidarrListCommand,
|
||||
},
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{Artist, DeleteParams, LidarrSerdeable},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_tag_command = LidarrCommand::Add(LidarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_system_status_command = LidarrCommand::Get(LidarrGetCommand::SystemStatus);
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteArtist(expected_delete_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_artist_command = LidarrCommand::Delete(LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, delete_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::ListArtists.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Artists(vec![
|
||||
Artist::default(),
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_artists_command = LidarrCommand::List(LidarrListCommand::Artists);
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, list_artists_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::UpdateAllArtists.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_series_command = LidarrCommand::Refresh(LidarrRefreshCommand::AllArtists);
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, refresh_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_toggle_artist_monitoring_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::ToggleArtistMonitoring(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let toggle_artist_monitoring_command = LidarrCommand::ToggleArtistMonitoring { artist_id: 1 };
|
||||
|
||||
let result = LidarrCliHandler::with(
|
||||
&app_arc,
|
||||
toggle_artist_monitoring_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_new_artist_command() {
|
||||
let expected_query = "test artist".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::SearchNewArtist(expected_query.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let search_new_artist_command = LidarrCommand::SearchNewArtist {
|
||||
query: expected_query,
|
||||
};
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, search_new_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
|
||||
{
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(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 =
|
||||
LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist {
|
||||
artist_id: 1,
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, arg};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "list_command_handler_tests.rs"]
|
||||
mod list_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrListCommand {
|
||||
#[command(about = "List all albums for the artist with the given ID")]
|
||||
Albums {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist whose albums you want to list",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
#[command(about = "List all artists in your Lidarr library")]
|
||||
Artists,
|
||||
#[command(about = "List all Lidarr metadata profiles")]
|
||||
MetadataProfiles,
|
||||
#[command(about = "List all Lidarr quality profiles")]
|
||||
QualityProfiles,
|
||||
#[command(about = "List all Lidarr tags")]
|
||||
Tags,
|
||||
}
|
||||
|
||||
impl From<LidarrListCommand> for Command {
|
||||
fn from(value: LidarrListCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::List(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrListCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrListCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrListCommand::Albums { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAlbums(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Artists => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ListArtists.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::MetadataProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetMetadataProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::QualityProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetQualityProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Tags => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTags.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, list_command_handler::LidarrListCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_list_command_from() {
|
||||
let command = LidarrListCommand::Artists;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::List(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_list_commands_have_no_arg_requirements(
|
||||
#[values("artists", "metadata-profiles", "quality-profiles", "tags")] subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_albums_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "albums"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_albums_with_artist_id() {
|
||||
let expected_args = LidarrListCommand::Albums { artist_id: 1 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "lidarr", "list", "albums", "--artist-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(album_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(album_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::lidarr::list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::LidarrSerdeable;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)]
|
||||
#[case(LidarrListCommand::MetadataProfiles, LidarrEvent::GetMetadataProfiles)]
|
||||
#[case(LidarrListCommand::QualityProfiles, LidarrEvent::GetQualityProfiles)]
|
||||
#[case(LidarrListCommand::Tags, LidarrEvent::GetTags)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_command(
|
||||
#[case] list_command: LidarrListCommand,
|
||||
#[case] expected_lidarr_event: LidarrEvent,
|
||||
) {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(expected_lidarr_event.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
|
||||
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_albums_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetAlbums(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_command = LidarrListCommand::Albums { artist_id: 1 };
|
||||
|
||||
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, arg};
|
||||
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
|
||||
use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler};
|
||||
use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
|
||||
use list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
|
||||
use tokio::sync::Mutex;
|
||||
use trigger_automatic_search_command_handler::{
|
||||
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
use super::{CliCommandHandler, Command};
|
||||
|
||||
mod add_command_handler;
|
||||
mod delete_command_handler;
|
||||
mod edit_command_handler;
|
||||
mod get_command_handler;
|
||||
mod list_command_handler;
|
||||
mod refresh_command_handler;
|
||||
mod trigger_automatic_search_command_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_command_tests.rs"]
|
||||
mod lidarr_command_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrCommand {
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to add or create new resources within your Lidarr instance"
|
||||
)]
|
||||
Add(LidarrAddCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to delete resources from your Lidarr instance"
|
||||
)]
|
||||
Delete(LidarrDeleteCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to edit resources in your Lidarr instance"
|
||||
)]
|
||||
Edit(LidarrEditCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to fetch details of the resources in your Lidarr instance"
|
||||
)]
|
||||
Get(LidarrGetCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to list attributes from your Lidarr instance"
|
||||
)]
|
||||
List(LidarrListCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to refresh the data in your Lidarr instance"
|
||||
)]
|
||||
Refresh(LidarrRefreshCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance"
|
||||
)]
|
||||
TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand),
|
||||
#[command(about = "Search for a new artist to add to Lidarr")]
|
||||
SearchNewArtist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The name of the artist you want to search for",
|
||||
required = true
|
||||
)]
|
||||
query: String,
|
||||
},
|
||||
#[command(
|
||||
about = "Toggle monitoring for the specified album corresponding to the given album ID"
|
||||
)]
|
||||
ToggleAlbumMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the album to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Toggle monitoring for the specified artist corresponding to the given artist ID"
|
||||
)]
|
||||
ToggleArtistMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrCommand> for Command {
|
||||
fn from(lidarr_command: LidarrCommand) -> Command {
|
||||
Command::Lidarr(lidarr_command)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrCliHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrCliHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrCommand::Add(add_command) => {
|
||||
LidarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::Delete(delete_command) => {
|
||||
LidarrDeleteCommandHandler::with(self.app, delete_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::Edit(edit_command) => {
|
||||
LidarrEditCommandHandler::with(self.app, edit_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::Get(get_command) => {
|
||||
LidarrGetCommandHandler::with(self.app, get_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::List(list_command) => {
|
||||
LidarrListCommandHandler::with(self.app, list_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::Refresh(refresh_command) => {
|
||||
LidarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
|
||||
LidarrTriggerAutomaticSearchCommandHandler::with(
|
||||
self.app,
|
||||
trigger_automatic_search_command,
|
||||
self.network,
|
||||
)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::SearchNewArtist { query } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::SearchNewArtist(query).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::ToggleAlbumMonitoring { album_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ToggleAlbumMonitoring(album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::ToggleArtistMonitoring { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ToggleArtistMonitoring(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "refresh_command_handler_tests.rs"]
|
||||
mod refresh_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrRefreshCommand {
|
||||
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
|
||||
AllArtists,
|
||||
#[command(about = "Refresh artist data and scan disk for the artist with the given ID")]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the artist to refresh information on and to scan the disk for",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrRefreshCommand> for Command {
|
||||
fn from(value: LidarrRefreshCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Refresh(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrRefreshCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
|
||||
for LidarrRefreshCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrRefreshCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> anyhow::Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrRefreshCommand::AllArtists => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::UpdateAllArtists.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrRefreshCommand::Artist { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::UpdateAndScanArtist(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, refresh_command_handler::LidarrRefreshCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_refresh_command_from() {
|
||||
let command = LidarrRefreshCommand::AllArtists;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Refresh(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_refresh_all_artists_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "all-artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_artist_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_artist_with_artist_id() {
|
||||
let expected_args = LidarrRefreshCommand::Artist { artist_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"refresh",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Lidarr(LidarrCommand::Refresh(refresh_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{app::App, cli::lidarr::refresh_command_handler::LidarrRefreshCommandHandler};
|
||||
use crate::{
|
||||
cli::{CliCommandHandler, lidarr::refresh_command_handler::LidarrRefreshCommand},
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
use crate::{
|
||||
models::{Serdeable, lidarr_models::LidarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_all_artists_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::UpdateAllArtists.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = LidarrRefreshCommand::AllArtists;
|
||||
|
||||
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_artist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::UpdateAndScanArtist(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = LidarrRefreshCommand::Artist { artist_id: 1 };
|
||||
|
||||
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "trigger_automatic_search_command_handler_tests.rs"]
|
||||
mod trigger_automatic_search_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrTriggerAutomaticSearchCommand {
|
||||
#[command(about = "Trigger an automatic search for the artist with the specified ID")]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the artist you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrTriggerAutomaticSearchCommand> for Command {
|
||||
fn from(value: LidarrTriggerAutomaticSearchCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrTriggerAutomaticSearchCommand>
|
||||
for LidarrTriggerAutomaticSearchCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrTriggerAutomaticSearchCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrTriggerAutomaticSearchCommand::Artist { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::TriggerAutomaticArtistSearch(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{
|
||||
LidarrCommand, trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand,
|
||||
},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_trigger_automatic_search_command_from() {
|
||||
let command = LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(command))
|
||||
);
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_artist_search_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"trigger-automatic-search",
|
||||
"artist",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_artist_search_with_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"trigger-automatic-search",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::lidarr::trigger_automatic_search_command_handler::{
|
||||
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
use crate::{app::App, cli::CliCommandHandler};
|
||||
use crate::{
|
||||
models::{Serdeable, lidarr_models::LidarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_artist_search_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(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::Artist { artist_id: 1 };
|
||||
|
||||
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap_complete::Shell;
|
||||
use lidarr::{LidarrCliHandler, LidarrCommand};
|
||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
@@ -24,6 +26,9 @@ pub enum Command {
|
||||
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
|
||||
Sonarr(SonarrCommand),
|
||||
|
||||
#[command(subcommand, about = "Commands for manging your Lidarr instance")]
|
||||
Lidarr(LidarrCommand),
|
||||
|
||||
#[command(
|
||||
arg_required_else_help = true,
|
||||
about = "Generate shell completions for the Managarr CLI"
|
||||
@@ -61,6 +66,11 @@ pub(crate) async fn handle_command(
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Command::Lidarr(lidarr_command) => {
|
||||
LidarrCliHandler::with(app, lidarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -122,12 +122,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
|
||||
title: String::new(),
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
minimum_availability: minimum_availability.to_string(),
|
||||
minimum_availability,
|
||||
monitored: !disable_monitoring,
|
||||
tags,
|
||||
tag_input_string: None,
|
||||
add_options: AddMovieOptions {
|
||||
monitor: monitor.to_string(),
|
||||
monitor,
|
||||
search_for_movie: !no_search_for_movie,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -384,12 +384,12 @@ mod tests {
|
||||
title: String::new(),
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
minimum_availability: "released".to_owned(),
|
||||
minimum_availability: MinimumAvailability::Released,
|
||||
monitored: false,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
add_options: AddMovieOptions {
|
||||
monitor: "movieAndCollection".to_owned(),
|
||||
monitor: MovieMonitor::MovieAndCollection,
|
||||
search_for_movie: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -137,12 +137,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
language_profile_id,
|
||||
series_type: series_type.to_string(),
|
||||
series_type,
|
||||
season_folder: !disable_season_folders,
|
||||
tags,
|
||||
tag_input_string: None,
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: monitor.to_string(),
|
||||
monitor,
|
||||
search_for_cutoff_unmet_episodes: !no_search_for_series,
|
||||
search_for_missing_episodes: !no_search_for_series,
|
||||
},
|
||||
|
||||
@@ -517,13 +517,13 @@ mod tests {
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
language_profile_id: 1,
|
||||
series_type: "anime".to_owned(),
|
||||
series_type: SeriesType::Anime,
|
||||
monitored: false,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
season_folder: false,
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: "future".to_owned(),
|
||||
monitor: SeriesMonitor::Future,
|
||||
search_for_cutoff_unmet_episodes: false,
|
||||
search_for_missing_episodes: false,
|
||||
},
|
||||
|
||||
@@ -4,13 +4,12 @@ mod property_tests {
|
||||
|
||||
use crate::app::App;
|
||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::{Scrollable, Paginated};
|
||||
use crate::models::{Paginated, Scrollable};
|
||||
|
||||
proptest! {
|
||||
/// Property test: Table never panics on index selection
|
||||
#[test]
|
||||
fn test_table_index_selection_safety(
|
||||
list_size in list_size(),
|
||||
@@ -25,19 +24,15 @@ mod property_tests {
|
||||
|
||||
table.set_items(movies);
|
||||
|
||||
// Try to select an arbitrary index
|
||||
if index < list_size {
|
||||
table.select_index(Some(index));
|
||||
let selected = table.current_selection();
|
||||
prop_assert_eq!(selected.id, index as i64);
|
||||
} else {
|
||||
// Out of bounds selection should be safe
|
||||
table.select_index(Some(index));
|
||||
// Should not panic, selection stays valid
|
||||
}
|
||||
}
|
||||
|
||||
/// Property test: Table state remains consistent after scroll operations
|
||||
#[test]
|
||||
fn test_table_scroll_consistency(
|
||||
list_size in list_size(),
|
||||
@@ -53,42 +48,34 @@ mod property_tests {
|
||||
table.set_items(movies);
|
||||
let initial_id = table.current_selection().id;
|
||||
|
||||
// Scroll down multiple times
|
||||
for _ in 0..scroll_amount {
|
||||
table.scroll_down();
|
||||
}
|
||||
let after_down_id = table.current_selection().id;
|
||||
|
||||
// Position should increase (up to max)
|
||||
prop_assert!(after_down_id >= initial_id);
|
||||
prop_assert!(after_down_id < list_size as i64);
|
||||
|
||||
// Scroll back up
|
||||
for _ in 0..scroll_amount {
|
||||
table.scroll_up();
|
||||
}
|
||||
|
||||
// Should return to initial position (or 0 if we hit the top)
|
||||
prop_assert!(table.current_selection().id <= initial_id);
|
||||
}
|
||||
|
||||
/// Property test: Empty tables handle operations gracefully
|
||||
#[test]
|
||||
fn test_empty_table_safety(_scroll_ops in 0usize..50) {
|
||||
let table = StatefulTable::<Movie>::default();
|
||||
|
||||
// Empty table operations should be safe
|
||||
prop_assert!(table.is_empty());
|
||||
prop_assert!(table.items.is_empty());
|
||||
}
|
||||
|
||||
/// Property test: Navigation operations maintain consistency
|
||||
#[test]
|
||||
fn test_navigation_consistency(pushes in 1usize..20) {
|
||||
let mut app = App::test_default();
|
||||
let initial_route = app.get_current_route();
|
||||
|
||||
// Push multiple routes
|
||||
let routes = vec![
|
||||
ActiveRadarrBlock::Movies,
|
||||
ActiveRadarrBlock::Collections,
|
||||
@@ -101,34 +88,27 @@ mod property_tests {
|
||||
app.push_navigation_stack(route.into());
|
||||
}
|
||||
|
||||
// Current route should be the last pushed
|
||||
let last_pushed = routes[(pushes - 1) % routes.len()];
|
||||
prop_assert_eq!(app.get_current_route(), last_pushed.into());
|
||||
|
||||
// Pop all routes
|
||||
for _ in 0..pushes {
|
||||
app.pop_navigation_stack();
|
||||
}
|
||||
|
||||
// Should return to initial route
|
||||
prop_assert_eq!(app.get_current_route(), initial_route);
|
||||
}
|
||||
|
||||
/// Property test: String input handling is safe
|
||||
#[test]
|
||||
fn test_string_input_safety(input in text_input_string()) {
|
||||
// String operations should never panic
|
||||
let _lowercase = input.to_lowercase();
|
||||
let _uppercase = input.to_uppercase();
|
||||
let _trimmed = input.trim();
|
||||
let _len = input.len();
|
||||
let _chars: Vec<char> = input.chars().collect();
|
||||
|
||||
// All operations completed without panic
|
||||
prop_assert!(true);
|
||||
}
|
||||
|
||||
/// Property test: Table maintains data integrity after operations
|
||||
#[test]
|
||||
fn test_table_data_integrity(
|
||||
list_size in 1usize..100
|
||||
@@ -144,16 +124,13 @@ mod property_tests {
|
||||
table.set_items(movies.clone());
|
||||
let original_count = table.items.len();
|
||||
|
||||
// Count should remain the same after various operations
|
||||
prop_assert_eq!(table.items.len(), original_count);
|
||||
|
||||
// All original items should still be present
|
||||
for movie in &movies {
|
||||
prop_assert!(table.items.iter().any(|m| m.id == movie.id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Property test: Page up/down maintains bounds
|
||||
#[test]
|
||||
fn test_page_navigation_bounds(
|
||||
list_size in list_size(),
|
||||
@@ -168,7 +145,6 @@ mod property_tests {
|
||||
|
||||
table.set_items(movies);
|
||||
|
||||
// Perform page operations
|
||||
for i in 0..page_ops {
|
||||
if i % 2 == 0 {
|
||||
table.page_down();
|
||||
@@ -176,14 +152,12 @@ mod property_tests {
|
||||
table.page_up();
|
||||
}
|
||||
|
||||
// Should never exceed bounds
|
||||
let current = table.current_selection();
|
||||
prop_assert!(current.id >= 0);
|
||||
prop_assert!(current.id < list_size as i64);
|
||||
}
|
||||
}
|
||||
|
||||
/// Property test: Table filtering reduces or maintains size
|
||||
#[test]
|
||||
fn test_table_filter_size_invariant(
|
||||
list_size in list_size(),
|
||||
@@ -200,7 +174,6 @@ mod property_tests {
|
||||
table.set_items(movies.clone());
|
||||
let original_size = table.items.len();
|
||||
|
||||
// Apply filter
|
||||
if !filter_term.is_empty() {
|
||||
let filtered: Vec<Movie> = movies.into_iter()
|
||||
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
|
||||
@@ -208,10 +181,8 @@ mod property_tests {
|
||||
table.set_items(filtered);
|
||||
}
|
||||
|
||||
// Filtered size should be <= original
|
||||
prop_assert!(table.items.len() <= original_size);
|
||||
|
||||
// Selection should still be valid if table not empty
|
||||
if !table.items.is_empty() {
|
||||
let current = table.current_selection();
|
||||
prop_assert!(current.id >= 0);
|
||||
|
||||
@@ -21,6 +21,7 @@ mod tests {
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
@@ -60,11 +61,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)]
|
||||
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)]
|
||||
fn test_handle_change_tabs<T>(#[case] index: usize, #[case] left_block: T, #[case] right_block: T)
|
||||
where
|
||||
#[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)]
|
||||
#[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)]
|
||||
#[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)]
|
||||
fn test_handle_change_tabs<T, U>(
|
||||
#[case] index: usize,
|
||||
#[case] left_block: T,
|
||||
#[case] right_block: U,
|
||||
) where
|
||||
T: Into<Route> + Copy,
|
||||
U: Into<Route> + Copy,
|
||||
{
|
||||
let mut app = App::test_default();
|
||||
app.error = "Test".into();
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -75,7 +76,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandle
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,616 @@
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions, AddArtistSearchResult};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ActiveLidarrBlock,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::AddArtistModal;
|
||||
use crate::models::{BlockSelectionState, Route, Scrollable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "add_artist_handler_tests.rs"]
|
||||
mod add_artist_handler_tests;
|
||||
|
||||
pub struct AddArtistHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl AddArtistHandler<'_, '_> {
|
||||
fn build_add_artist_body(&mut self) -> AddArtistBody {
|
||||
let add_artist_modal = self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.take()
|
||||
.expect("AddArtistModal is None");
|
||||
let tags = add_artist_modal.tags.text;
|
||||
let AddArtistModal {
|
||||
root_folder_list,
|
||||
monitor_list,
|
||||
monitor_new_items_list,
|
||||
quality_profile_list,
|
||||
metadata_profile_list,
|
||||
..
|
||||
} = add_artist_modal;
|
||||
let (foreign_artist_id, artist_name) = {
|
||||
let AddArtistSearchResult {
|
||||
foreign_artist_id,
|
||||
artist_name,
|
||||
..
|
||||
} = self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_searched_artists
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.current_selection();
|
||||
(foreign_artist_id.clone(), artist_name.text.clone())
|
||||
};
|
||||
let quality_profile = quality_profile_list.current_selection();
|
||||
let quality_profile_id = *self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.quality_profile_map
|
||||
.iter()
|
||||
.filter(|(_, value)| *value == quality_profile)
|
||||
.map(|(key, _)| key)
|
||||
.next()
|
||||
.unwrap();
|
||||
let metadata_profile = metadata_profile_list.current_selection();
|
||||
let metadata_profile_id = *self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.metadata_profile_map
|
||||
.iter()
|
||||
.filter(|(_, value)| *value == metadata_profile)
|
||||
.map(|(key, _)| key)
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
let path = root_folder_list.current_selection().path.clone();
|
||||
let monitor = *monitor_list.current_selection();
|
||||
let monitor_new_items = *monitor_new_items_list.current_selection();
|
||||
|
||||
AddArtistBody {
|
||||
foreign_artist_id,
|
||||
artist_name,
|
||||
monitored: true,
|
||||
root_folder_path: path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
tags: Vec::new(),
|
||||
tag_input_string: Some(tags),
|
||||
add_options: AddArtistOptions {
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
search_for_missing_albums: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let add_artist_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveLidarrBlock::AddArtistSearchResults.into());
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| {
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_searched_artists
|
||||
.as_mut()
|
||||
.expect("add_searched_artists should be initialized")
|
||||
},
|
||||
add_artist_table_handling_config,
|
||||
) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
ADD_ARTIST_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
) -> AddArtistHandler<'a, 'b> {
|
||||
AddArtistHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
_context: context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.scroll_up(),
|
||||
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_new_items_list
|
||||
.scroll_up(),
|
||||
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.scroll_up(),
|
||||
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.scroll_up(),
|
||||
ActiveLidarrBlock::AddArtistSelectRootFolder => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.root_folder_list
|
||||
.scroll_up(),
|
||||
ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.up(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.scroll_down(),
|
||||
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_new_items_list
|
||||
.scroll_down(),
|
||||
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.scroll_down(),
|
||||
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.scroll_down(),
|
||||
ActiveLidarrBlock::AddArtistSelectRootFolder => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.root_folder_list
|
||||
.scroll_down(),
|
||||
ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.down(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.scroll_to_top(),
|
||||
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_new_items_list
|
||||
.scroll_to_top(),
|
||||
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.scroll_to_top(),
|
||||
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.scroll_to_top(),
|
||||
ActiveLidarrBlock::AddArtistSelectRootFolder => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.root_folder_list
|
||||
.scroll_to_top(),
|
||||
ActiveLidarrBlock::AddArtistSearchInput => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_search
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.scroll_home(),
|
||||
ActiveLidarrBlock::AddArtistTagsInput => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
.scroll_home(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_end(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.scroll_to_bottom(),
|
||||
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_new_items_list
|
||||
.scroll_to_bottom(),
|
||||
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.scroll_to_bottom(),
|
||||
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.scroll_to_bottom(),
|
||||
ActiveLidarrBlock::AddArtistSelectRootFolder => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.root_folder_list
|
||||
.scroll_to_bottom(),
|
||||
ActiveLidarrBlock::AddArtistSearchInput => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_search
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.reset_offset(),
|
||||
ActiveLidarrBlock::AddArtistTagsInput => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
.reset_offset(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AddArtistPrompt => handle_prompt_toggle(self.app, self.key),
|
||||
ActiveLidarrBlock::AddArtistSearchInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_search
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistTagsInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AddArtistSearchInput
|
||||
if !self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_search
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.text
|
||||
.is_empty() =>
|
||||
{
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistSearchResults
|
||||
if self.app.data.lidarr_data.add_searched_artists.is_some() =>
|
||||
{
|
||||
let foreign_artist_id = self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_searched_artists
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.current_selection()
|
||||
.foreign_artist_id
|
||||
.clone();
|
||||
|
||||
if self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.items
|
||||
.iter()
|
||||
.any(|artist| artist.foreign_artist_id == foreign_artist_id)
|
||||
{
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into());
|
||||
} else {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into());
|
||||
self.app.data.lidarr_data.add_artist_modal = Some((&self.app.data.lidarr_data).into());
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS);
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistPrompt => {
|
||||
match self.app.data.lidarr_data.selected_block.get_active_block() {
|
||||
ActiveLidarrBlock::AddArtistConfirmPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::AddArtist(self.build_add_artist_body()));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
|
||||
| ActiveLidarrBlock::AddArtistSelectQualityProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.push_navigation_stack(
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.get_active_block()
|
||||
.into(),
|
||||
),
|
||||
ActiveLidarrBlock::AddArtistTagsInput => {
|
||||
self.app.push_navigation_stack(
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.get_active_block()
|
||||
.into(),
|
||||
);
|
||||
self.app.ignore_special_keys_for_textbox_input = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
|
||||
| ActiveLidarrBlock::AddArtistSelectQualityProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(),
|
||||
ActiveLidarrBlock::AddArtistTagsInput => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AddArtistSearchInput => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.add_artist_search = None;
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistSearchResults
|
||||
| ActiveLidarrBlock::AddArtistEmptySearchResults => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.add_searched_artists = None;
|
||||
self.app.ignore_special_keys_for_textbox_input = true;
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.add_artist_modal = None;
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
|
||||
| ActiveLidarrBlock::AddArtistSelectQualityProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
|
||||
| ActiveLidarrBlock::AddArtistAlreadyInLibrary
|
||||
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(),
|
||||
ActiveLidarrBlock::AddArtistTagsInput => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AddArtistSearchInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_search
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistTagsInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistPrompt => {
|
||||
if self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::AddArtistConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::AddArtist(self.build_add_artist_body()));
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::lidarr_models::Album;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS,
|
||||
EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "artist_details_handler_tests.rs"]
|
||||
mod artist_details_handler_tests;
|
||||
|
||||
pub struct ArtistDetailsHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl ArtistDetailsHandler<'_, '_> {
|
||||
fn extract_artist_id(&self) -> i64 {
|
||||
self.app.data.lidarr_data.artists.current_selection().id
|
||||
}
|
||||
|
||||
fn extract_album_id(&self) -> i64 {
|
||||
self.app.data.lidarr_data.albums.current_selection().id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let albums_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveLidarrBlock::ArtistDetails.into())
|
||||
.searching_block(ActiveLidarrBlock::SearchAlbums.into())
|
||||
.search_error_block(ActiveLidarrBlock::SearchAlbumsError.into())
|
||||
.search_field_fn(|album: &Album| &album.title.text);
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.lidarr_data.albums,
|
||||
albums_table_handling_config,
|
||||
) {
|
||||
match self.active_lidarr_block {
|
||||
_ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => {
|
||||
DeleteAlbumHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
DeleteAlbumHandler::accepts(active_block) || ARTIST_DETAILS_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
) -> ArtistDetailsHandler<'a, 'b> {
|
||||
ArtistDetailsHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::ArtistDetails {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
handle_prompt_toggle(self.app, self.key);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()),
|
||||
);
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id()));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
ActiveLidarrBlock::ArtistDetails => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.reset_artist_info_tabs();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::ArtistDetails => match self.key {
|
||||
_ if matches_key!(refresh, key) => self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_lidarr_block.into()),
|
||||
_ if matches_key!(auto_search, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into());
|
||||
}
|
||||
_ if matches_key!(update, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
|
||||
}
|
||||
_ if matches_key!(edit, key) => {
|
||||
self.app.push_navigation_stack(
|
||||
(
|
||||
ActiveLidarrBlock::EditArtistPrompt,
|
||||
Some(self.active_lidarr_block),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into());
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
|
||||
}
|
||||
_ if matches_key!(toggle_monitoring, key) => {
|
||||
if !self.app.data.lidarr_data.albums.is_empty() {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::ToggleAlbumMonitoring(self.extract_album_id()));
|
||||
|
||||
self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()),
|
||||
);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id()));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
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::servarr_data::lidarr::lidarr_data::{
|
||||
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
|
||||
};
|
||||
|
||||
mod test_handle_delete {
|
||||
use super::*;
|
||||
use crate::assert_delete_prompt;
|
||||
use crate::event::Key;
|
||||
use crate::models::lidarr_models::Album;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
|
||||
|
||||
#[test]
|
||||
fn test_album_delete() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.albums
|
||||
.set_items(vec![Album::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
assert_delete_prompt!(
|
||||
ArtistDetailsHandler,
|
||||
app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt
|
||||
);
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.blocks,
|
||||
DELETE_ALBUM_SELECTION_BLOCKS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
|
||||
#[rstest]
|
||||
fn test_left_right_prompt_toggle(
|
||||
#[values(
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
#[values(Key::Left, Key::Right)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
ArtistDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
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 rstest::rstest;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(1)
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
|
||||
LidarrEvent::UpdateAndScanArtist(1)
|
||||
)]
|
||||
fn test_artist_details_prompt_confirm_submit(
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
#[case] expected_action: LidarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_prompt_decline_submit(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
)]
|
||||
prompt_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use rstest::rstest;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_esc(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
)]
|
||||
prompt_block: ActiveLidarrBlock,
|
||||
#[values(true, false)] is_ready: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_char_key_event {
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
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::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
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)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.edit.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(
|
||||
app,
|
||||
(
|
||||
ActiveLidarrBlock::EditArtistPrompt,
|
||||
Some(ActiveLidarrBlock::ArtistDetails)
|
||||
)
|
||||
.into()
|
||||
);
|
||||
assert_modal_present!(app.data.lidarr_data.edit_artist_modal);
|
||||
assert!(app.data.lidarr_data.edit_artist_modal.is_some());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.blocks,
|
||||
EDIT_ARTIST_SELECTION_BLOCKS
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_edit_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.edit.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
assert_modal_absent!(app.data.lidarr_data.edit_artist_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_toggle_monitoring_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_routing = false;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::ArtistDetails.into()
|
||||
);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(app.is_routing);
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::ToggleAlbumMonitoring(1))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_toggle_monitoring_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.lidarr_data.prompt_confirm = false;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::ArtistDetails.into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_toggle_monitoring_key_no_op_when_albums_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(app.data.lidarr_data.prompt_confirm_action.is_none());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_auto_search_key(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.auto_search.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(
|
||||
app,
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_auto_search_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.auto_search.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_update_key(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.update.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_update_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.update.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_refresh_key(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_routing = false;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, active_lidarr_block.into());
|
||||
assert!(app.is_routing);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_refresh_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.is_routing = false;
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(1)
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
|
||||
LidarrEvent::UpdateAndScanArtist(1)
|
||||
)]
|
||||
fn test_artist_details_prompt_confirm_key(
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
#[case] expected_action: LidarrEvent,
|
||||
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
prompt_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert_navigation_popped!(app, active_lidarr_block.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_handler_accepts() {
|
||||
let mut artist_details_blocks = ARTIST_DETAILS_BLOCKS.clone().to_vec();
|
||||
artist_details_blocks.extend(DELETE_ALBUM_BLOCKS);
|
||||
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if artist_details_blocks.contains(&active_lidarr_block) {
|
||||
assert!(ArtistDetailsHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!ArtistDetailsHandler::accepts(active_lidarr_block));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_artist_id() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
|
||||
let artist_id = ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.extract_artist_id();
|
||||
|
||||
assert_eq!(artist_id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_album_id() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
|
||||
let album_id = ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.extract_album_id();
|
||||
|
||||
assert_eq!(album_id, 1);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_artist_details_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
|
||||
let handler = ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_handler_is_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_handler_is_ready_when_not_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::DeleteParams;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_BLOCKS;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
event::Key,
|
||||
handlers::{KeyEventHandler, handle_prompt_toggle},
|
||||
matches_key,
|
||||
models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_album_handler_tests.rs"]
|
||||
mod delete_album_handler_tests;
|
||||
|
||||
pub(in crate::handlers::lidarr_handlers) struct DeleteAlbumHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl DeleteAlbumHandler<'_, '_> {
|
||||
fn build_delete_album_params(&mut self) -> DeleteParams {
|
||||
let id = self.app.data.lidarr_data.albums.current_selection().id;
|
||||
let delete_files = self.app.data.lidarr_data.delete_files;
|
||||
let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion;
|
||||
self.app.data.lidarr_data.reset_delete_preferences();
|
||||
|
||||
DeleteParams {
|
||||
id,
|
||||
delete_files,
|
||||
add_import_list_exclusion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteAlbumHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
DELETE_ALBUM_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
) -> Self {
|
||||
DeleteAlbumHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
|
||||
self.app.data.lidarr_data.selected_block.up();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
|
||||
self.app.data.lidarr_data.selected_block.down();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
|
||||
handle_prompt_toggle(self.app, self.key);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
|
||||
match self.app.data.lidarr_data.selected_block.get_active_block() {
|
||||
ActiveLidarrBlock::DeleteAlbumConfirmPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::DeleteAlbum(self.build_delete_album_params()));
|
||||
self.app.should_refresh = true;
|
||||
} else {
|
||||
self.app.data.lidarr_data.reset_delete_preferences();
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile => {
|
||||
self.app.data.lidarr_data.delete_files = !self.app.data.lidarr_data.delete_files;
|
||||
}
|
||||
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion => {
|
||||
self.app.data.lidarr_data.add_import_list_exclusion =
|
||||
!self.app.data.lidarr_data.add_import_list_exclusion;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.reset_delete_preferences();
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt
|
||||
&& self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::DeleteAlbumConfirmPrompt
|
||||
&& matches_key!(confirm, self.key)
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::DeleteAlbum(self.build_delete_album_params()));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
|
||||
use crate::models::lidarr_models::{Album, DeleteParams};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ALBUM_BLOCKS};
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_album_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
app.data.lidarr_data.selected_block.down();
|
||||
|
||||
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
|
||||
|
||||
if key == Key::Up {
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteAlbumConfirmPrompt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_album_prompt_scroll_no_op_when_not_ready(
|
||||
#[values(Key::Up, Key::Down)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
app.data.lidarr_data.selected_block.down();
|
||||
|
||||
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
|
||||
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_prompt_prompt_decline_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteAlbumHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_confirm_prompt_prompt_confirmation_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.albums
|
||||
.set_items(vec![Album::default()]);
|
||||
let expected_delete_album_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
DeleteAlbumHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::DeleteAlbum(expected_delete_album_params))
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteAlbumHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt.into()
|
||||
);
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert!(!app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(app.data.lidarr_data.delete_files);
|
||||
assert!(app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_toggle_delete_files_submit() {
|
||||
let current_route = ActiveLidarrBlock::DeleteAlbumPrompt.into();
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
|
||||
DeleteAlbumHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), current_route);
|
||||
assert_eq!(app.data.lidarr_data.delete_files, true);
|
||||
|
||||
DeleteAlbumHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), current_route);
|
||||
assert_eq!(app.data.lidarr_data.delete_files, false);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
use rstest::rstest;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_album_prompt_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteAlbumHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use crate::{
|
||||
assert_navigation_popped,
|
||||
models::{
|
||||
BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS,
|
||||
},
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_confirm_prompt_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.albums
|
||||
.set_items(vec![Album::default()]);
|
||||
let expected_delete_album_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
DeleteAlbumHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::DeleteAlbum(expected_delete_album_params))
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_handler_accepts() {
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(DeleteAlbumHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!DeleteAlbumHandler::accepts(active_lidarr_block));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_album_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
let handler = DeleteAlbumHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_delete_album_params() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.albums
|
||||
.set_items(vec![Album::default()]);
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
let expected_delete_album_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
|
||||
let delete_album_params = DeleteAlbumHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.build_delete_album_params();
|
||||
|
||||
assert_eq!(delete_album_params, expected_delete_album_params);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = DeleteAlbumHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_handler_ready_when_not_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = DeleteAlbumHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::DeleteParams;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
event::Key,
|
||||
handlers::{KeyEventHandler, handle_prompt_toggle},
|
||||
matches_key,
|
||||
models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_artist_handler_tests.rs"]
|
||||
mod delete_artist_handler_tests;
|
||||
|
||||
pub(in crate::handlers::lidarr_handlers) struct DeleteArtistHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl DeleteArtistHandler<'_, '_> {
|
||||
fn build_delete_artist_params(&mut self) -> DeleteParams {
|
||||
let id = self.app.data.lidarr_data.artists.current_selection().id;
|
||||
let delete_files = self.app.data.lidarr_data.delete_files;
|
||||
let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion;
|
||||
self.app.data.lidarr_data.reset_delete_preferences();
|
||||
|
||||
DeleteParams {
|
||||
id,
|
||||
delete_files,
|
||||
add_import_list_exclusion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
DELETE_ARTIST_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
) -> Self {
|
||||
DeleteArtistHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
self.app.data.lidarr_data.selected_block.up();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
self.app.data.lidarr_data.selected_block.down();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
handle_prompt_toggle(self.app, self.key);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
match self.app.data.lidarr_data.selected_block.get_active_block() {
|
||||
ActiveLidarrBlock::DeleteArtistConfirmPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params()));
|
||||
self.app.should_refresh = true;
|
||||
} else {
|
||||
self.app.data.lidarr_data.reset_delete_preferences();
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::DeleteArtistToggleDeleteFile => {
|
||||
self.app.data.lidarr_data.delete_files = !self.app.data.lidarr_data.delete_files;
|
||||
}
|
||||
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion => {
|
||||
self.app.data.lidarr_data.add_import_list_exclusion =
|
||||
!self.app.data.lidarr_data.add_import_list_exclusion;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.reset_delete_preferences();
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt
|
||||
&& self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::DeleteArtistConfirmPrompt
|
||||
&& matches_key!(confirm, self.key)
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params()));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::delete_artist_handler::DeleteArtistHandler;
|
||||
use crate::models::lidarr_models::{Artist, DeleteParams};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS};
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app.data.lidarr_data.selected_block.down();
|
||||
|
||||
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
|
||||
|
||||
if key == Key::Up {
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteArtistToggleDeleteFile
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteArtistConfirmPrompt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_artist_prompt_scroll_no_op_when_not_ready(
|
||||
#[values(Key::Up, Key::Down)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app.data.lidarr_data.selected_block.down();
|
||||
|
||||
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.get_active_block(),
|
||||
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
|
||||
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_prompt_prompt_decline_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_confirm_prompt_prompt_confirmation_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::DeleteArtist(expected_delete_artist_params))
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::DeleteArtistPrompt.into()
|
||||
);
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert!(!app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(app.data.lidarr_data.delete_files);
|
||||
assert!(app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_toggle_delete_files_submit() {
|
||||
let current_route = ActiveLidarrBlock::DeleteArtistPrompt.into();
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), current_route);
|
||||
assert_eq!(app.data.lidarr_data.delete_files, true);
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), current_route);
|
||||
assert_eq!(app.data.lidarr_data.delete_files, false);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
use rstest::rstest;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_artist_prompt_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use crate::{
|
||||
assert_navigation_popped,
|
||||
models::{
|
||||
BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS,
|
||||
},
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_confirm_prompt_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.selected_block
|
||||
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::DeleteArtist(expected_delete_artist_params))
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_handler_accepts() {
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(DeleteArtistHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!DeleteArtistHandler::accepts(active_lidarr_block));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_artist_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
let handler = DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_delete_artist_params() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
|
||||
let delete_artist_params = DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.build_delete_artist_params();
|
||||
|
||||
assert_eq!(delete_artist_params, expected_delete_artist_params);
|
||||
assert!(!app.data.lidarr_data.delete_files);
|
||||
assert!(!app.data.lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_handler_ready_when_not_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = DeleteArtistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::lidarr_models::EditArtistParams;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
|
||||
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_artist_handler_tests.rs"]
|
||||
mod edit_artist_handler_tests;
|
||||
|
||||
pub(in crate::handlers::lidarr_handlers) struct EditArtistHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl EditArtistHandler<'_, '_> {
|
||||
fn build_edit_artist_params(&mut self) -> EditArtistParams {
|
||||
let edit_artist_modal = self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.take()
|
||||
.expect("EditArtistModal is None");
|
||||
let artist_id = self.app.data.lidarr_data.artists.current_selection().id;
|
||||
let tags = edit_artist_modal.tags.text;
|
||||
|
||||
let EditArtistModal {
|
||||
monitored,
|
||||
path,
|
||||
monitor_list,
|
||||
quality_profile_list,
|
||||
metadata_profile_list,
|
||||
..
|
||||
} = edit_artist_modal;
|
||||
let quality_profile = quality_profile_list.current_selection();
|
||||
let quality_profile_id = *self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.quality_profile_map
|
||||
.iter()
|
||||
.filter(|(_, value)| *value == quality_profile)
|
||||
.map(|(key, _)| key)
|
||||
.next()
|
||||
.unwrap();
|
||||
let metadata_profile = metadata_profile_list.current_selection();
|
||||
let metadata_profile_id = *self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.metadata_profile_map
|
||||
.iter()
|
||||
.filter(|(_, value)| *value == metadata_profile)
|
||||
.map(|(key, _)| key)
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
EditArtistParams {
|
||||
artist_id,
|
||||
monitored,
|
||||
monitor_new_items: Some(*monitor_list.current_selection()),
|
||||
quality_profile_id: Some(quality_profile_id),
|
||||
metadata_profile_id: Some(metadata_profile_id),
|
||||
root_folder_path: Some(path.text),
|
||||
tag_input_string: Some(tags),
|
||||
..EditArtistParams::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditArtistHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
EDIT_ARTIST_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
) -> EditArtistHandler<'a, 'b> {
|
||||
EditArtistHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading && self.app.data.lidarr_data.edit_artist_modal.is_some()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.scroll_up(),
|
||||
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.scroll_up(),
|
||||
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.scroll_up(),
|
||||
ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.up(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.scroll_down(),
|
||||
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.scroll_down(),
|
||||
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.scroll_down(),
|
||||
ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.down(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.scroll_to_top(),
|
||||
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.scroll_to_top(),
|
||||
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.scroll_to_top(),
|
||||
ActiveLidarrBlock::EditArtistPathInput => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.path
|
||||
.scroll_home(),
|
||||
ActiveLidarrBlock::EditArtistTagsInput => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
.scroll_home(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_end(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.scroll_to_bottom(),
|
||||
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.scroll_to_bottom(),
|
||||
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.scroll_to_bottom(),
|
||||
ActiveLidarrBlock::EditArtistPathInput => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.path
|
||||
.reset_offset(),
|
||||
ActiveLidarrBlock::EditArtistTagsInput => self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
.reset_offset(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::EditArtistPrompt => handle_prompt_toggle(self.app, self.key),
|
||||
ActiveLidarrBlock::EditArtistPathInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.path
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistTagsInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::EditArtistPrompt => {
|
||||
match self.app.data.lidarr_data.selected_block.get_active_block() {
|
||||
ActiveLidarrBlock::EditArtistConfirmPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::EditArtist(self.build_edit_artist_params()));
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
|
||||
| ActiveLidarrBlock::EditArtistSelectQualityProfile
|
||||
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.push_navigation_stack(
|
||||
(
|
||||
self.app.data.lidarr_data.selected_block.get_active_block(),
|
||||
self.context,
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => {
|
||||
self.app.push_navigation_stack(
|
||||
(
|
||||
self.app.data.lidarr_data.selected_block.get_active_block(),
|
||||
self.context,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
self.app.ignore_special_keys_for_textbox_input = true;
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistToggleMonitored => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitored = Some(
|
||||
!self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitored
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
|
||||
| ActiveLidarrBlock::EditArtistSelectQualityProfile
|
||||
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(),
|
||||
ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::EditArtistTagsInput | ActiveLidarrBlock::EditArtistPathInput => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.edit_artist_modal = None;
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
|
||||
| ActiveLidarrBlock::EditArtistSelectQualityProfile
|
||||
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::EditArtistPathInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.path
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistTagsInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.edit_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistPrompt => {
|
||||
if self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::EditArtistConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::EditArtist(self.build_edit_artist_params()));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,717 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
use serde_json::Number;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options};
|
||||
use crate::models::lidarr_models::{Album, Artist, ArtistStatistics, ArtistStatus};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
|
||||
DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
assert_modal_absent, assert_modal_present, assert_navigation_popped, assert_navigation_pushed,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_library_handler_accepts() {
|
||||
let mut library_handler_blocks = Vec::new();
|
||||
library_handler_blocks.extend(LIBRARY_BLOCKS);
|
||||
library_handler_blocks.extend(ARTIST_DETAILS_BLOCKS);
|
||||
library_handler_blocks.extend(DELETE_ARTIST_BLOCKS);
|
||||
library_handler_blocks.extend(DELETE_ALBUM_BLOCKS);
|
||||
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
|
||||
library_handler_blocks.extend(ADD_ARTIST_BLOCKS);
|
||||
|
||||
ActiveLidarrBlock::iter().for_each(|lidarr_block| {
|
||||
if library_handler_blocks.contains(&lidarr_block) {
|
||||
assert!(LibraryHandler::accepts(lidarr_block));
|
||||
} else {
|
||||
assert!(!LibraryHandler::accepts(lidarr_block));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_name() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.artist_name
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.artist_name.text.to_lowercase())
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[0].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_type() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.artist_type
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_lowercase()
|
||||
.cmp(
|
||||
&b.artist_type
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_lowercase(),
|
||||
)
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[1].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Type");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_status() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.status
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.cmp(&b.status.to_string().to_lowercase())
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[2].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Status");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_quality_profile() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering =
|
||||
|a, b| a.quality_profile_id.cmp(&b.quality_profile_id);
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[3].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Quality Profile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_metadata_profile() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering =
|
||||
|a, b| a.metadata_profile_id.cmp(&b.metadata_profile_id);
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[4].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Metadata Profile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_albums() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.album_count)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.album_count))
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[5].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Albums");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_tracks() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.track_count)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.track_count))
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[6].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Tracks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_size() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.size_on_disk)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.size_on_disk))
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[7].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Size");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_monitored() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| a.monitored.cmp(&b.monitored);
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[8].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Monitored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_tags() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
let a_str = a
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| tag.as_i64().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
let b_str = b
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| tag.as_i64().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
a_str.cmp(&b_str)
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[9].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Tags");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_monitoring_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.is_routing = false;
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert!(app.is_routing);
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&LidarrEvent::ToggleArtistMonitoring(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_monitoring_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.is_routing = false;
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_modal_absent!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_artists_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.update.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_artists_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.update.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_artists_prompt_confirm_submit() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.submit.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&LidarrEvent::UpdateAllArtists
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_artists_prompt_decline_submit() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.submit.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_artists_prompt_esc() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_artists_prompt_left_right() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_artists_prompt_confirm_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&LidarrEvent::UpdateAllArtists
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
fn artists_vec() -> Vec<Artist> {
|
||||
vec![
|
||||
Artist {
|
||||
id: 3,
|
||||
artist_name: "Test Artist 1".into(),
|
||||
artist_type: Some("Group".to_owned()),
|
||||
status: ArtistStatus::Ended,
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
monitored: false,
|
||||
tags: vec![Number::from(1), Number::from(2)],
|
||||
statistics: Some(ArtistStatistics {
|
||||
album_count: 5,
|
||||
track_count: 50,
|
||||
size_on_disk: 789,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
},
|
||||
Artist {
|
||||
id: 2,
|
||||
artist_name: "Test Artist 2".into(),
|
||||
artist_type: Some("Solo".to_owned()),
|
||||
status: ArtistStatus::Continuing,
|
||||
quality_profile_id: 2,
|
||||
metadata_profile_id: 2,
|
||||
monitored: false,
|
||||
tags: vec![Number::from(1), Number::from(3)],
|
||||
statistics: Some(ArtistStatistics {
|
||||
album_count: 10,
|
||||
track_count: 100,
|
||||
size_on_disk: 456,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
},
|
||||
Artist {
|
||||
id: 1,
|
||||
artist_name: "Test Artist 3".into(),
|
||||
artist_type: None,
|
||||
status: ArtistStatus::Deleted,
|
||||
quality_profile_id: 3,
|
||||
metadata_profile_id: 3,
|
||||
monitored: true,
|
||||
tags: vec![Number::from(2), Number::from(3)],
|
||||
statistics: Some(ArtistStatistics {
|
||||
album_count: 3,
|
||||
track_count: 30,
|
||||
size_on_disk: 123,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_add_artist_blocks_to_add_artist_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AddArtistSearchInput,
|
||||
ActiveLidarrBlock::AddArtistEmptySearchResults,
|
||||
ActiveLidarrBlock::AddArtistSearchResults
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[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());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::ArtistDetails.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegates_delete_artist_blocks_to_delete_artist_handler() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_edit_artist_blocks_to_edit_artist_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::EditArtistPrompt,
|
||||
ActiveLidarrBlock::EditArtistSelectMetadataProfile,
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems,
|
||||
ActiveLidarrBlock::EditArtistSelectQualityProfile,
|
||||
ActiveLidarrBlock::EditArtistTagsInput,
|
||||
ActiveLidarrBlock::EditArtistPathInput
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_artist_details_blocks_to_artist_details_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
ActiveLidarrBlock::SearchAlbums,
|
||||
ActiveLidarrBlock::SearchAlbumsError,
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.data.lidarr_data.quality_profile_map =
|
||||
bimap::BiMap::from_iter([(0i64, "Default Quality".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
bimap::BiMap::from_iter([(0i64, "Default Metadata".to_owned())]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.edit.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::EditArtistPrompt.into());
|
||||
assert_modal_present!(app.data.lidarr_data.edit_artist_modal);
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.selected_block.blocks,
|
||||
EDIT_ARTIST_SELECTION_BLOCKS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.edit.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
assert_modal_absent!(app.data.lidarr_data.edit_artist_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
use crate::{
|
||||
app::App,
|
||||
event::Key,
|
||||
handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle},
|
||||
matches_key,
|
||||
models::{
|
||||
BlockSelectionState, HorizontallyScrollableText,
|
||||
lidarr_models::Artist,
|
||||
servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
LIBRARY_BLOCKS,
|
||||
},
|
||||
stateful_table::SortOption,
|
||||
},
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
|
||||
use super::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
|
||||
mod add_artist_handler;
|
||||
mod artist_details_handler;
|
||||
mod delete_album_handler;
|
||||
mod delete_artist_handler;
|
||||
mod edit_artist_handler;
|
||||
|
||||
use crate::models::Route;
|
||||
pub(in crate::handlers::lidarr_handlers) use add_artist_handler::AddArtistHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use artist_details_handler::ArtistDetailsHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "library_handler_tests.rs"]
|
||||
mod library_handler_tests;
|
||||
|
||||
pub(super) struct LibraryHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl LibraryHandler<'_, '_> {
|
||||
fn extract_artist_id(&self) -> i64 {
|
||||
self.app.data.lidarr_data.artists.current_selection().id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let artists_table_handling_config = TableHandlingConfig::new(ActiveLidarrBlock::Artists.into())
|
||||
.sorting_block(ActiveLidarrBlock::ArtistsSortPrompt.into())
|
||||
.sort_options(artists_sorting_options())
|
||||
.searching_block(ActiveLidarrBlock::SearchArtists.into())
|
||||
.search_error_block(ActiveLidarrBlock::SearchArtistsError.into())
|
||||
.search_field_fn(|artist| &artist.artist_name.text)
|
||||
.filtering_block(ActiveLidarrBlock::FilterArtists.into())
|
||||
.filter_error_block(ActiveLidarrBlock::FilterArtistsError.into())
|
||||
.filter_field_fn(|artist| &artist.artist_name.text);
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.lidarr_data.artists,
|
||||
artists_table_handling_config,
|
||||
) {
|
||||
match self.active_lidarr_block {
|
||||
_ if AddArtistHandler::accepts(self.active_lidarr_block) => {
|
||||
AddArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if DeleteArtistHandler::accepts(self.active_lidarr_block) => {
|
||||
DeleteArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if EditArtistHandler::accepts(self.active_lidarr_block) => {
|
||||
EditArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if ArtistDetailsHandler::accepts(self.active_lidarr_block) => {
|
||||
ArtistDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
AddArtistHandler::accepts(active_block)
|
||||
|| DeleteArtistHandler::accepts(active_block)
|
||||
|| EditArtistHandler::accepts(active_block)
|
||||
|| ArtistDetailsHandler::accepts(active_block)
|
||||
|| LIBRARY_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
) -> LibraryHandler<'a, 'b> {
|
||||
LibraryHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading && !self.app.data.lidarr_data.artists.is_empty()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::Artists {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::Artists => handle_change_tab_left_right_keys(self.app, self.key),
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt => handle_prompt_toggle(self.app, self.key),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::Artists => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
}
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
_ => {
|
||||
handle_clear_errors(self.app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::Artists => match key {
|
||||
_ if matches_key!(add, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into());
|
||||
self.app.data.lidarr_data.add_artist_search = Some(HorizontallyScrollableText::default());
|
||||
self.app.ignore_special_keys_for_textbox_input = true;
|
||||
}
|
||||
_ if matches_key!(toggle_monitoring, key) => {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(
|
||||
LidarrEvent::ToggleArtistMonitoring(self.extract_artist_id()),
|
||||
);
|
||||
|
||||
self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
|
||||
}
|
||||
_ if matches_key!(edit, key) => {
|
||||
self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into());
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
|
||||
}
|
||||
_ if matches_key!(update, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
|
||||
}
|
||||
_ if matches_key!(refresh, key) => {
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
fn artists_sorting_options() -> Vec<SortOption<Artist>> {
|
||||
vec![
|
||||
SortOption {
|
||||
name: "Name",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.artist_name
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.artist_name.text.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Type",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.artist_type
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_lowercase()
|
||||
.cmp(
|
||||
&b.artist_type
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_lowercase(),
|
||||
)
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Status",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.status
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.cmp(&b.status.to_string().to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Quality Profile",
|
||||
cmp_fn: Some(|a, b| a.quality_profile_id.cmp(&b.quality_profile_id)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Metadata Profile",
|
||||
cmp_fn: Some(|a, b| a.metadata_profile_id.cmp(&b.metadata_profile_id)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Albums",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.album_count)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.album_count))
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Tracks",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.track_count)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.track_count))
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Size",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.size_on_disk)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.size_on_disk))
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Monitored",
|
||||
cmp_fn: Some(|a, b| a.monitored.cmp(&b.monitored)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Tags",
|
||||
cmp_fn: Some(|a, b| {
|
||||
let a_str = a
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| tag.as_i64().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
let b_str = b
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| tag.as_i64().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
|
||||
a_str.cmp(&b_str)
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::LidarrHandler;
|
||||
use crate::models::lidarr_models::Artist;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
let handler = LidarrHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_handler_is_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = LidarrHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_handler_accepts() {
|
||||
for lidarr_block in ActiveLidarrBlock::iter() {
|
||||
assert!(LidarrHandler::accepts(lidarr_block));
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_library_blocks_to_library_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::Artists,
|
||||
ActiveLidarrBlock::ArtistsSortPrompt,
|
||||
ActiveLidarrBlock::FilterArtists,
|
||||
ActiveLidarrBlock::FilterArtistsError,
|
||||
ActiveLidarrBlock::SearchArtists,
|
||||
ActiveLidarrBlock::SearchArtistsError,
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
ActiveLidarrBlock::EditArtistPrompt,
|
||||
ActiveLidarrBlock::EditArtistPathInput,
|
||||
ActiveLidarrBlock::EditArtistSelectMetadataProfile,
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems,
|
||||
ActiveLidarrBlock::EditArtistSelectQualityProfile,
|
||||
ActiveLidarrBlock::EditArtistTagsInput
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
LidarrHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use library::LibraryHandler;
|
||||
|
||||
use super::KeyEventHandler;
|
||||
use crate::models::Route;
|
||||
use crate::{
|
||||
app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
|
||||
};
|
||||
|
||||
mod library;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_handler_tests.rs"]
|
||||
mod lidarr_handler_tests;
|
||||
|
||||
pub(super) struct LidarrHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
_ if LibraryHandler::accepts(self.active_lidarr_block) => {
|
||||
LibraryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(_active_block: ActiveLidarrBlock) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
) -> LidarrHandler<'a, 'b> {
|
||||
LidarrHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {}
|
||||
|
||||
fn handle_submit(&mut self) {}
|
||||
|
||||
fn handle_esc(&mut self) {}
|
||||
|
||||
fn handle_char_key_event(&mut self) {}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: Key) {
|
||||
let key_ref = key;
|
||||
match key_ref {
|
||||
_ if matches_key!(left, key, app.ignore_special_keys_for_textbox_input) => {
|
||||
app.data.lidarr_data.main_tabs.previous();
|
||||
app.pop_and_push_navigation_stack(app.data.lidarr_data.main_tabs.get_active_route());
|
||||
}
|
||||
_ if matches_key!(right, key, app.ignore_special_keys_for_textbox_input) => {
|
||||
app.data.lidarr_data.main_tabs.next();
|
||||
app.pop_and_push_navigation_stack(app.data.lidarr_data.main_tabs.get_active_route());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use lidarr_handlers::LidarrHandler;
|
||||
use radarr_handlers::RadarrHandler;
|
||||
use sonarr_handlers::SonarrHandler;
|
||||
|
||||
@@ -15,6 +16,7 @@ use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{HorizontallyScrollableText, Route};
|
||||
|
||||
mod keybinding_handler;
|
||||
mod lidarr_handlers;
|
||||
mod radarr_handlers;
|
||||
mod sonarr_handlers;
|
||||
|
||||
@@ -125,6 +127,9 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
||||
Route::Sonarr(active_sonarr_block, context) => {
|
||||
SonarrHandler::new(key, app, active_sonarr_block, context).handle()
|
||||
}
|
||||
Route::Lidarr(active_lidarr_block, context) => {
|
||||
LidarrHandler::new(key, app, active_lidarr_block, context).handle()
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -187,6 +192,9 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
|
||||
Route::Sonarr(_, _) => {
|
||||
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
|
||||
}
|
||||
Route::Lidarr(_, _) => {
|
||||
app.data.lidarr_data.prompt_confirm = !app.data.lidarr_data.prompt_confirm
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::radarr_models::BlocklistItem;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
||||
EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "collection_details_handler_tests.rs"]
|
||||
@@ -148,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::radarr_models::EditCollectionParams;
|
||||
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -376,7 +376,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::radarr_models::Collection;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
mod collection_details_handler;
|
||||
@@ -179,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
|
||||
use crate::models::servarr_models::EditIndexerParams;
|
||||
@@ -527,7 +528,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::radarr_models::IndexerSettings;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||
@@ -293,7 +294,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestA
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
mod edit_indexer_handler;
|
||||
@@ -212,7 +212,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::models::servarr_data::radarr::modals::AddMovieModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::models::{BlockSelectionState, Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -83,8 +83,8 @@ impl AddMovieHandler<'_, '_> {
|
||||
.unwrap();
|
||||
|
||||
let path = root_folder_list.current_selection().path.clone();
|
||||
let monitor = monitor_list.current_selection().to_string();
|
||||
let minimum_availability = minimum_availability_list.current_selection().to_string();
|
||||
let monitor = *monitor_list.current_selection();
|
||||
let minimum_availability = *minimum_availability_list.current_selection();
|
||||
|
||||
AddMovieBody {
|
||||
tmdb_id,
|
||||
@@ -558,7 +558,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::radarr_models::DeleteMovieParams;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
@@ -141,7 +142,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::radarr_models::EditMovieParams;
|
||||
use crate::models::servarr_data::radarr::modals::EditMovieModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS};
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -397,7 +397,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
|
||||
};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::models::{BlockSelectionState, Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -379,7 +379,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::handlers::radarr_handlers::indexers::IndexersHandler;
|
||||
use crate::handlers::radarr_handlers::library::LibraryHandler;
|
||||
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
|
||||
use crate::handlers::radarr_handlers::system::SystemHandler;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::{App, Key, matches_key};
|
||||
|
||||
@@ -112,7 +113,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie,
|
||||
DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection,
|
||||
MovieFile, RadarrRelease, Rating, RatingsList,
|
||||
MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList,
|
||||
};
|
||||
use crate::models::servarr_models::{
|
||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||
@@ -470,13 +470,13 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
||||
tmdb_id: 1234,
|
||||
title: "Test".to_owned(),
|
||||
root_folder_path: "/nfs2".to_owned(),
|
||||
minimum_availability: "announced".to_owned(),
|
||||
minimum_availability: MinimumAvailability::Announced,
|
||||
monitored: true,
|
||||
quality_profile_id: 2222,
|
||||
tags: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".into()),
|
||||
add_options: AddMovieOptions {
|
||||
monitor: "movieOnly".to_owned(),
|
||||
monitor: MovieMonitor::MovieOnly,
|
||||
search_for_movie: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS};
|
||||
use crate::models::servarr_models::AddRootFolderBody;
|
||||
use crate::models::{HorizontallyScrollableText, Route};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -231,7 +231,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||
use crate::matches_key;
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::{Route, Scrollable};
|
||||
|
||||
mod system_details_handler;
|
||||
|
||||
@@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::radarr_models::RadarrTaskName;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::sonarr_models::BlocklistItem;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
|
||||
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::sonarr_models::SonarrHistoryItem;
|
||||
@@ -121,7 +122,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
|
||||
use crate::models::servarr_models::EditIndexerParams;
|
||||
@@ -526,7 +527,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||
};
|
||||
@@ -202,7 +203,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ use crate::handlers::sonarr_handlers::indexers::test_all_indexers_handler::TestA
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
|
||||
mod edit_indexer_handler;
|
||||
@@ -211,7 +211,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, ActiveSonarrBlock,
|
||||
};
|
||||
use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult};
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::models::{BlockSelectionState, Route, Scrollable};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -74,8 +74,8 @@ impl AddSeriesHandler<'_, '_> {
|
||||
.unwrap();
|
||||
|
||||
let path = root_folder_list.current_selection().path.clone();
|
||||
let monitor = monitor_list.current_selection().to_string();
|
||||
let series_type = series_type_list.current_selection().to_string();
|
||||
let monitor = *monitor_list.current_selection();
|
||||
let series_type = *series_type_list.current_selection();
|
||||
|
||||
AddSeriesBody {
|
||||
tvdb_id,
|
||||
@@ -426,8 +426,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_sonarr_block {
|
||||
_ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchInput
|
||||
&& !self
|
||||
ActiveSonarrBlock::AddSeriesSearchInput
|
||||
if !self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
@@ -442,8 +442,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
|
||||
.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into());
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
_ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchResults
|
||||
&& self.app.data.sonarr_data.add_searched_series.is_some() =>
|
||||
ActiveSonarrBlock::AddSeriesSearchResults
|
||||
if self.app.data.sonarr_data.add_searched_series.is_some() =>
|
||||
{
|
||||
let tvdb_id = self
|
||||
.app
|
||||
@@ -625,7 +625,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,8 +1030,6 @@ mod tests {
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveSonarrBlock::Series.into());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into());
|
||||
let mut add_searched_series = StatefulTable::default();
|
||||
add_searched_series.set_items(vec![AddSeriesSearchResult::default()]);
|
||||
|
||||
AddSeriesHandler::new(
|
||||
SUBMIT_KEY,
|
||||
@@ -1053,6 +1051,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::Series.into());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into());
|
||||
|
||||
AddSeriesHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
@@ -1092,7 +1091,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_prompt_prompt_decline_submit() {
|
||||
fn test_add_series_confirm_prompt_prompt_decline_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::Series.into());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into());
|
||||
@@ -1169,12 +1168,12 @@ mod tests {
|
||||
root_folder_path: "/nfs2".to_owned(),
|
||||
quality_profile_id: 2222,
|
||||
language_profile_id: 2222,
|
||||
series_type: "standard".to_owned(),
|
||||
series_type: SeriesType::Standard,
|
||||
season_folder: true,
|
||||
tags: Vec::default(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: "all".to_owned(),
|
||||
monitor: SeriesMonitor::All,
|
||||
search_for_cutoff_unmet_episodes: true,
|
||||
search_for_missing_episodes: true,
|
||||
},
|
||||
@@ -1195,9 +1194,9 @@ mod tests {
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveSonarrBlock::Series.into());
|
||||
assert_eq!(
|
||||
app.data.sonarr_data.prompt_confirm_action,
|
||||
Some(SonarrEvent::AddSeries(expected_add_series_body))
|
||||
assert_some_eq_x!(
|
||||
&app.data.sonarr_data.prompt_confirm_action,
|
||||
&SonarrEvent::AddSeries(expected_add_series_body.clone())
|
||||
);
|
||||
assert_modal_absent!(app.data.sonarr_data.add_series_modal);
|
||||
}
|
||||
@@ -1647,12 +1646,12 @@ mod tests {
|
||||
root_folder_path: "/nfs2".to_owned(),
|
||||
quality_profile_id: 2222,
|
||||
language_profile_id: 2222,
|
||||
series_type: "standard".to_owned(),
|
||||
series_type: SeriesType::Standard,
|
||||
season_folder: true,
|
||||
tags: Vec::default(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: "all".to_owned(),
|
||||
monitor: SeriesMonitor::All,
|
||||
search_for_cutoff_unmet_episodes: true,
|
||||
search_for_missing_episodes: true,
|
||||
},
|
||||
@@ -1777,12 +1776,12 @@ mod tests {
|
||||
root_folder_path: "/nfs2".to_owned(),
|
||||
quality_profile_id: 2222,
|
||||
language_profile_id: 2222,
|
||||
series_type: "standard".to_owned(),
|
||||
series_type: SeriesType::Standard,
|
||||
season_folder: true,
|
||||
tags: Vec::default(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: "all".to_owned(),
|
||||
monitor: SeriesMonitor::All,
|
||||
search_for_cutoff_unmet_episodes: true,
|
||||
search_for_missing_episodes: true,
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::models::Route;
|
||||
use crate::models::sonarr_models::DeleteSeriesParams;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{
|
||||
@@ -143,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::servarr_data::sonarr::modals::EditSeriesModal;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS};
|
||||
use crate::models::sonarr_models::EditSeriesParams;
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -471,7 +471,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::library::season_details_handler::releases_
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS};
|
||||
use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
@@ -370,7 +371,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandle
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use crate::handlers::sonarr_handlers::library::episode_details_handler::EpisodeD
|
||||
use crate::handlers::sonarr_handlers::library::season_details_handler::SeasonDetailsHandler;
|
||||
use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::models::Route;
|
||||
|
||||
mod add_series_handler;
|
||||
mod delete_series_handler;
|
||||
@@ -245,7 +246,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::history::history_sorting_options;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::sonarr_models::{
|
||||
@@ -458,7 +459,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ use crate::handlers::sonarr_handlers::history::history_sorting_options;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -327,10 +327,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler
|
||||
}
|
||||
}
|
||||
ActiveSonarrBlock::UpdateAndScanSeriesPrompt => {
|
||||
if self.app.data.sonarr_data.prompt_confirm {
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id()));
|
||||
}
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id()));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
@@ -342,7 +341,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ mod tests {
|
||||
ActiveSonarrBlock::SeriesDetails.into()
|
||||
);
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_modal_absent!(app.data.sonarr_data.prompt_confirm_action);
|
||||
assert_none!(app.data.sonarr_data.prompt_confirm_action);
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
|
||||
@@ -555,7 +555,6 @@ mod tests {
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.sonarr_data.prompt_confirm = true;
|
||||
app.data.sonarr_data.series.set_items(vec![series()]);
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
@@ -6,12 +6,12 @@ use library::LibraryHandler;
|
||||
use root_folders::RootFoldersHandler;
|
||||
use system::SystemHandler;
|
||||
|
||||
use super::KeyEventHandler;
|
||||
use crate::models::Route;
|
||||
use crate::{
|
||||
app::App, event::Key, matches_key, models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
|
||||
};
|
||||
|
||||
use super::KeyEventHandler;
|
||||
|
||||
mod blocklist;
|
||||
mod downloads;
|
||||
mod history;
|
||||
@@ -115,7 +115,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use crate::event::Key;
|
||||
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS};
|
||||
use crate::models::servarr_models::AddRootFolderBody;
|
||||
use crate::models::{HorizontallyScrollableText, Route};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -229,7 +229,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::sonarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||
use crate::matches_key;
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::{Route, Scrollable};
|
||||
|
||||
mod system_details_handler;
|
||||
|
||||
@@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemHandler<'a, 'b
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS};
|
||||
use crate::models::sonarr_models::SonarrTaskName;
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod tests {
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::TableHandlingConfig;
|
||||
use crate::handlers::table_handler::handle_table;
|
||||
use crate::models::Route;
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::servarr_models::Language;
|
||||
@@ -98,7 +99,7 @@ mod tests {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
+22
-9
@@ -3,12 +3,15 @@
|
||||
extern crate assertables;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version};
|
||||
use clap::{
|
||||
Args, CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version,
|
||||
};
|
||||
use clap_complete::generate;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use log::{debug, error, warn};
|
||||
use network::NetworkTrait;
|
||||
use ratatui::Terminal;
|
||||
@@ -64,6 +67,13 @@ mod utils;
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
#[command(flatten)]
|
||||
global: GlobalOpts,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
#[command(next_help_heading = "Global Options")]
|
||||
struct GlobalOpts {
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
@@ -98,9 +108,12 @@ struct Cli {
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
help = "For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
||||
This is useful when you have multiple instances of the same Servarr defined in your config file.
|
||||
By default, if left empty, the first configured Servarr instance listed in the config file will be used."
|
||||
help = indoc!{"
|
||||
For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
||||
|
||||
This is useful when you have multiple instances of the same Servarr defined in your config file.
|
||||
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
|
||||
"}
|
||||
)]
|
||||
servarr_name: Option<String>,
|
||||
}
|
||||
@@ -114,13 +127,13 @@ async fn main() -> Result<()> {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
let args = Cli::parse();
|
||||
let mut config = if let Some(ref config_file) = args.config_file {
|
||||
let mut config = if let Some(ref config_file) = args.global.config_file {
|
||||
load_config(config_file.to_str().expect("Invalid config file specified"))?
|
||||
} else {
|
||||
confy::load("managarr", "config")?
|
||||
};
|
||||
let theme_name = config.theme.clone();
|
||||
let spinner_disabled = args.disable_spinner;
|
||||
let spinner_disabled = args.global.disable_spinner;
|
||||
debug!("Managarr loaded using config: {config:?}");
|
||||
config.validate();
|
||||
config.post_process_initialization();
|
||||
@@ -145,7 +158,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
match args.command {
|
||||
Some(command) => match command {
|
||||
Command::Radarr(_) | Command::Sonarr(_) => {
|
||||
Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) => {
|
||||
if spinner_disabled {
|
||||
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||
} else {
|
||||
@@ -165,8 +178,8 @@ async fn main() -> Result<()> {
|
||||
});
|
||||
start_ui(
|
||||
&app,
|
||||
&args.themes_file,
|
||||
args.theme.unwrap_or(theme_name.unwrap_or_default()),
|
||||
&args.global.themes_file,
|
||||
args.global.theme.unwrap_or(theme_name.unwrap_or_default()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use derivative::Derivative;
|
||||
use enum_display_style_derive::EnumDisplayStyle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value};
|
||||
use strum::{Display, EnumIter};
|
||||
|
||||
use super::{
|
||||
HorizontallyScrollableText, Serdeable,
|
||||
servarr_models::{DiskSpace, HostConfig, QualityProfile, RootFolder, SecurityConfig, Tag},
|
||||
};
|
||||
use crate::serde_enum_from;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_models_tests.rs"]
|
||||
mod lidarr_models_tests;
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Artist {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub artist_name: HorizontallyScrollableText,
|
||||
pub foreign_artist_id: String,
|
||||
pub status: ArtistStatus,
|
||||
pub overview: Option<String>,
|
||||
pub artist_type: Option<String>,
|
||||
pub disambiguation: Option<String>,
|
||||
pub members: Option<Vec<Member>>,
|
||||
pub path: String,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub quality_profile_id: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub metadata_profile_id: i64,
|
||||
pub monitored: bool,
|
||||
pub monitor_new_items: NewItemMonitorType,
|
||||
pub genres: Vec<String>,
|
||||
pub tags: Vec<Number>,
|
||||
pub added: DateTime<Utc>,
|
||||
pub ratings: Option<Ratings>,
|
||||
pub statistics: Option<ArtistStatistics>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, Display, EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum ArtistStatus {
|
||||
#[default]
|
||||
Continuing,
|
||||
Ended,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Ratings {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub votes: i64,
|
||||
#[serde(deserialize_with = "super::from_f64")]
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
impl Eq for Ratings {}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Member {
|
||||
pub name: Option<String>,
|
||||
pub instrument: Option<String>,
|
||||
}
|
||||
|
||||
impl Eq for Member {}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ArtistStatistics {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub album_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub track_file_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub track_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub total_track_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub size_on_disk: i64,
|
||||
#[serde(deserialize_with = "super::from_f64")]
|
||||
pub percent_of_tracks: f64,
|
||||
}
|
||||
|
||||
impl Eq for ArtistStatistics {}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct MetadataProfile {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<(&i64, &String)> for MetadataProfile {
|
||||
fn from(value: (&i64, &String)) -> Self {
|
||||
MetadataProfile {
|
||||
id: *value.0,
|
||||
name: value.1.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
EnumIter,
|
||||
clap::ValueEnum,
|
||||
Display,
|
||||
EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum NewItemMonitorType {
|
||||
#[default]
|
||||
#[display_style(name = "All Albums")]
|
||||
All,
|
||||
#[display_style(name = "No New Albums")]
|
||||
None,
|
||||
#[display_style(name = "New Albums")]
|
||||
New,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
EnumIter,
|
||||
clap::ValueEnum,
|
||||
Display,
|
||||
EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum MonitorType {
|
||||
#[default]
|
||||
#[display_style(name = "All Albums")]
|
||||
All,
|
||||
#[display_style(name = "Future Albums")]
|
||||
Future,
|
||||
#[display_style(name = "Missing Albums")]
|
||||
Missing,
|
||||
#[display_style(name = "Existing Albums")]
|
||||
Existing,
|
||||
#[display_style(name = "First Album")]
|
||||
First,
|
||||
#[display_style(name = "Latest Album")]
|
||||
Latest,
|
||||
None,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadRecord {
|
||||
pub title: String,
|
||||
pub status: DownloadStatus,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub album_id: Option<Number>,
|
||||
pub artist_id: Option<Number>,
|
||||
#[serde(deserialize_with = "super::from_f64")]
|
||||
pub size: f64,
|
||||
#[serde(deserialize_with = "super::from_f64")]
|
||||
pub sizeleft: f64,
|
||||
pub output_path: Option<HorizontallyScrollableText>,
|
||||
#[serde(default)]
|
||||
pub indexer: String,
|
||||
pub download_client: Option<String>,
|
||||
}
|
||||
|
||||
impl Eq for DownloadRecord {}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
EnumIter,
|
||||
Display,
|
||||
EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum DownloadStatus {
|
||||
#[default]
|
||||
Unknown,
|
||||
Queued,
|
||||
Paused,
|
||||
Downloading,
|
||||
Completed,
|
||||
Failed,
|
||||
Warning,
|
||||
Delay,
|
||||
#[display_style(name = "Download Client Unavailable")]
|
||||
DownloadClientUnavailable,
|
||||
Fallback,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadsResponse {
|
||||
pub records: Vec<DownloadRecord>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemStatus {
|
||||
pub version: String,
|
||||
pub start_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddArtistSearchResult {
|
||||
pub foreign_artist_id: String,
|
||||
pub artist_name: HorizontallyScrollableText,
|
||||
pub status: ArtistStatus,
|
||||
pub overview: Option<String>,
|
||||
pub artist_type: Option<String>,
|
||||
pub disambiguation: Option<String>,
|
||||
pub genres: Vec<String>,
|
||||
pub ratings: Option<Ratings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LidarrCommandBody {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub artist_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct DeleteParams {
|
||||
pub id: i64,
|
||||
pub delete_files: bool,
|
||||
pub add_import_list_exclusion: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddArtistBody {
|
||||
pub foreign_artist_id: String,
|
||||
pub artist_name: String,
|
||||
pub monitored: bool,
|
||||
pub root_folder_path: String,
|
||||
pub quality_profile_id: i64,
|
||||
pub metadata_profile_id: i64,
|
||||
pub tags: Vec<i64>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub tag_input_string: Option<String>,
|
||||
pub add_options: AddArtistOptions,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddArtistOptions {
|
||||
pub monitor: MonitorType,
|
||||
pub monitor_new_items: NewItemMonitorType,
|
||||
pub search_for_missing_albums: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditArtistParams {
|
||||
pub artist_id: i64,
|
||||
pub monitored: Option<bool>,
|
||||
pub monitor_new_items: Option<NewItemMonitorType>,
|
||||
pub quality_profile_id: Option<i64>,
|
||||
pub metadata_profile_id: Option<i64>,
|
||||
pub root_folder_path: Option<String>,
|
||||
pub tags: Option<Vec<i64>>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub tag_input_string: Option<String>,
|
||||
pub clear_tags: bool,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Album {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub title: HorizontallyScrollableText,
|
||||
pub foreign_album_id: String,
|
||||
pub monitored: bool,
|
||||
#[serde(default)]
|
||||
pub any_release_ok: bool,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub profile_id: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub duration: i64,
|
||||
pub album_type: Option<String>,
|
||||
pub genres: Vec<String>,
|
||||
pub ratings: Option<Ratings>,
|
||||
pub release_date: Option<DateTime<Utc>>,
|
||||
pub statistics: Option<AlbumStatistics>,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AlbumStatistics {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub track_file_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub track_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub total_track_count: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub size_on_disk: i64,
|
||||
#[serde(deserialize_with = "super::from_f64")]
|
||||
pub percent_of_tracks: f64,
|
||||
}
|
||||
|
||||
impl Eq for AlbumStatistics {}
|
||||
|
||||
impl From<LidarrSerdeable> for Serdeable {
|
||||
fn from(value: LidarrSerdeable) -> Serdeable {
|
||||
Serdeable::Lidarr(value)
|
||||
}
|
||||
}
|
||||
|
||||
serde_enum_from!(
|
||||
LidarrSerdeable {
|
||||
AddArtistSearchResults(Vec<AddArtistSearchResult>),
|
||||
Albums(Vec<Album>),
|
||||
Album(Album),
|
||||
Artist(Artist),
|
||||
Artists(Vec<Artist>),
|
||||
DiskSpaces(Vec<DiskSpace>),
|
||||
DownloadsResponse(DownloadsResponse),
|
||||
HostConfig(HostConfig),
|
||||
MetadataProfiles(Vec<MetadataProfile>),
|
||||
QualityProfiles(Vec<QualityProfile>),
|
||||
RootFolders(Vec<RootFolder>),
|
||||
SecurityConfig(SecurityConfig),
|
||||
SystemStatus(SystemStatus),
|
||||
Tag(Tag),
|
||||
Tags(Vec<Tag>),
|
||||
Value(Value),
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,543 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Utc;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::models::lidarr_models::{
|
||||
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
|
||||
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
|
||||
};
|
||||
use crate::models::servarr_models::{
|
||||
DiskSpace, HostConfig, QualityProfile, RootFolder, SecurityConfig, Tag,
|
||||
};
|
||||
use crate::models::{
|
||||
Serdeable,
|
||||
lidarr_models::{Artist, ArtistStatistics, ArtistStatus, LidarrSerdeable, Ratings},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_artist_status_default() {
|
||||
assert_eq!(ArtistStatus::default(), ArtistStatus::Continuing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_item_monitor_type_display() {
|
||||
assert_str_eq!(NewItemMonitorType::All.to_string(), "all");
|
||||
assert_str_eq!(NewItemMonitorType::None.to_string(), "none");
|
||||
assert_str_eq!(NewItemMonitorType::New.to_string(), "new");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_item_monitor_type_to_display_str() {
|
||||
assert_str_eq!(NewItemMonitorType::All.to_display_str(), "All Albums");
|
||||
assert_str_eq!(NewItemMonitorType::None.to_display_str(), "No New Albums");
|
||||
assert_str_eq!(NewItemMonitorType::New.to_display_str(), "New Albums");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_monitor_type_display() {
|
||||
assert_str_eq!(MonitorType::All.to_string(), "all");
|
||||
assert_str_eq!(MonitorType::Future.to_string(), "future");
|
||||
assert_str_eq!(MonitorType::Missing.to_string(), "missing");
|
||||
assert_str_eq!(MonitorType::Existing.to_string(), "existing");
|
||||
assert_str_eq!(MonitorType::First.to_string(), "first");
|
||||
assert_str_eq!(MonitorType::Latest.to_string(), "latest");
|
||||
assert_str_eq!(MonitorType::None.to_string(), "none");
|
||||
assert_str_eq!(MonitorType::Unknown.to_string(), "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_monitor_type_to_display_str() {
|
||||
assert_str_eq!(MonitorType::All.to_display_str(), "All Albums");
|
||||
assert_str_eq!(MonitorType::Future.to_display_str(), "Future Albums");
|
||||
assert_str_eq!(MonitorType::Missing.to_display_str(), "Missing Albums");
|
||||
assert_str_eq!(MonitorType::Existing.to_display_str(), "Existing Albums");
|
||||
assert_str_eq!(MonitorType::First.to_display_str(), "First Album");
|
||||
assert_str_eq!(MonitorType::Latest.to_display_str(), "Latest Album");
|
||||
assert_str_eq!(MonitorType::None.to_display_str(), "None");
|
||||
assert_str_eq!(MonitorType::Unknown.to_display_str(), "Unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from() {
|
||||
let lidarr_serdeable = LidarrSerdeable::Value(json!({}));
|
||||
|
||||
let serdeable: Serdeable = Serdeable::from(lidarr_serdeable.clone());
|
||||
|
||||
assert_eq!(serdeable, Serdeable::Lidarr(lidarr_serdeable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_unit() {
|
||||
let lidarr_serdeable = LidarrSerdeable::from(());
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Value(json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_value() {
|
||||
let value = json!({"test": "test"});
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = value.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Value(value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_artists() {
|
||||
let artists = vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = artists.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Artists(artists));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_deserialization() {
|
||||
let artist_json = json!({
|
||||
"id": 1,
|
||||
"artistName": "Test Artist",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"overview": "Test overview",
|
||||
"artistType": "Group",
|
||||
"disambiguation": "UK Band",
|
||||
"path": "/music/test-artist",
|
||||
"members": [
|
||||
{ "name": "alex", "instrument": "piano" },
|
||||
{ "name": "madi", "instrument": "vocals" }
|
||||
],
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"monitorNewItems": "all",
|
||||
"genres": ["Rock", "Alternative"],
|
||||
"tags": [1, 2],
|
||||
"added": "2023-01-01T00:00:00Z",
|
||||
"ratings": {
|
||||
"votes": 100,
|
||||
"value": 4.5
|
||||
},
|
||||
"statistics": {
|
||||
"albumCount": 5,
|
||||
"trackFileCount": 50,
|
||||
"trackCount": 60,
|
||||
"totalTrackCount": 70,
|
||||
"sizeOnDisk": 1000000000,
|
||||
"percentOfTracks": 83.33
|
||||
}
|
||||
});
|
||||
let expected_members_vec = vec![
|
||||
Member {
|
||||
name: Some("alex".to_string()),
|
||||
instrument: Some("piano".to_string()),
|
||||
},
|
||||
Member {
|
||||
name: Some("madi".to_string()),
|
||||
instrument: Some("vocals".to_string()),
|
||||
},
|
||||
];
|
||||
|
||||
let artist: Artist = serde_json::from_value(artist_json).unwrap();
|
||||
|
||||
assert_eq!(artist.id, 1);
|
||||
assert_str_eq!(artist.artist_name.text, "Test Artist");
|
||||
assert_str_eq!(artist.foreign_artist_id, "test-foreign-id");
|
||||
assert_eq!(artist.status, ArtistStatus::Continuing);
|
||||
assert_some_eq_x!(&artist.overview, "Test overview");
|
||||
assert_some_eq_x!(&artist.artist_type, "Group");
|
||||
assert_some_eq_x!(&artist.disambiguation, "UK Band");
|
||||
assert_str_eq!(artist.path, "/music/test-artist");
|
||||
assert_some_eq_x!(&artist.members, &expected_members_vec);
|
||||
assert_eq!(artist.quality_profile_id, 1);
|
||||
assert_eq!(artist.metadata_profile_id, 1);
|
||||
assert!(artist.monitored);
|
||||
assert_eq!(artist.monitor_new_items, NewItemMonitorType::All);
|
||||
assert_eq!(artist.genres, vec!["Rock", "Alternative"]);
|
||||
assert_eq!(artist.tags.len(), 2);
|
||||
assert_some!(&artist.ratings);
|
||||
assert_some!(&artist.statistics);
|
||||
|
||||
let ratings = artist.ratings.unwrap();
|
||||
assert_eq!(ratings.votes, 100);
|
||||
assert_eq!(ratings.value, 4.5);
|
||||
|
||||
let stats = artist.statistics.unwrap();
|
||||
assert_eq!(stats.album_count, 5);
|
||||
assert_eq!(stats.track_file_count, 50);
|
||||
assert_eq!(stats.track_count, 60);
|
||||
assert_eq!(stats.total_track_count, 70);
|
||||
assert_eq!(stats.size_on_disk, 1000000000);
|
||||
assert_eq!(stats.percent_of_tracks, 83.33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_status_deserialization() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ArtistStatus>("\"continuing\"").unwrap(),
|
||||
ArtistStatus::Continuing
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ArtistStatus>("\"ended\"").unwrap(),
|
||||
ArtistStatus::Ended
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ArtistStatus>("\"deleted\"").unwrap(),
|
||||
ArtistStatus::Deleted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ratings_equality() {
|
||||
let ratings1 = Ratings {
|
||||
votes: 100,
|
||||
value: 4.5,
|
||||
};
|
||||
let ratings2 = Ratings {
|
||||
votes: 100,
|
||||
value: 4.5,
|
||||
};
|
||||
let ratings3 = Ratings {
|
||||
votes: 50,
|
||||
value: 3.0,
|
||||
};
|
||||
|
||||
assert_eq!(ratings1, ratings2);
|
||||
assert_ne!(ratings1, ratings3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_statistics_equality() {
|
||||
let stats1 = ArtistStatistics {
|
||||
album_count: 5,
|
||||
track_file_count: 50,
|
||||
track_count: 60,
|
||||
total_track_count: 70,
|
||||
size_on_disk: 1000000000,
|
||||
percent_of_tracks: 83.33,
|
||||
};
|
||||
let stats2 = ArtistStatistics {
|
||||
album_count: 5,
|
||||
track_file_count: 50,
|
||||
track_count: 60,
|
||||
total_track_count: 70,
|
||||
size_on_disk: 1000000000,
|
||||
percent_of_tracks: 83.33,
|
||||
};
|
||||
let stats3 = ArtistStatistics::default();
|
||||
|
||||
assert_eq!(stats1, stats2);
|
||||
assert_ne!(stats1, stats3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_with_optional_fields_none() {
|
||||
let artist_json = json!({
|
||||
"id": 1,
|
||||
"artistName": "Test Artist",
|
||||
"foreignArtistId": "",
|
||||
"status": "continuing",
|
||||
"path": "",
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": false,
|
||||
"monitorNewItems": "all",
|
||||
"genres": [],
|
||||
"tags": [],
|
||||
"added": "2023-01-01T00:00:00Z"
|
||||
});
|
||||
|
||||
let artist: Artist = serde_json::from_value(artist_json).unwrap();
|
||||
|
||||
assert_none!(&artist.overview);
|
||||
assert_none!(&artist.artist_type);
|
||||
assert_none!(&artist.disambiguation);
|
||||
assert_eq!(artist.monitor_new_items, NewItemMonitorType::All);
|
||||
assert_none!(&artist.ratings);
|
||||
assert_none!(&artist.statistics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_artist() {
|
||||
let artist = Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
};
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = artist.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Artist(artist));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_disk_spaces() {
|
||||
let disk_spaces = vec![DiskSpace {
|
||||
free_space: 1,
|
||||
total_space: 1,
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = disk_spaces.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::DiskSpaces(disk_spaces));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_downloads_response() {
|
||||
let downloads_response = DownloadsResponse {
|
||||
records: vec![DownloadRecord {
|
||||
id: 1,
|
||||
..DownloadRecord::default()
|
||||
}],
|
||||
};
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = downloads_response.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::DownloadsResponse(downloads_response)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_metadata_profiles() {
|
||||
let metadata_profiles = vec![MetadataProfile {
|
||||
id: 1,
|
||||
name: "Standard".to_owned(),
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = metadata_profiles.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::MetadataProfiles(metadata_profiles)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_host_config() {
|
||||
let host_config = HostConfig {
|
||||
port: 8686,
|
||||
..HostConfig::default()
|
||||
};
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = host_config.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::HostConfig(host_config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_quality_profiles() {
|
||||
let quality_profiles = vec![QualityProfile {
|
||||
id: 1,
|
||||
name: "Any".to_owned(),
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = quality_profiles.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::QualityProfiles(quality_profiles)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_root_folders() {
|
||||
let root_folders = vec![RootFolder {
|
||||
id: 1,
|
||||
path: "/music".to_owned(),
|
||||
accessible: true,
|
||||
free_space: 1000000,
|
||||
unmapped_folders: None,
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = root_folders.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::RootFolders(root_folders));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_security_config() {
|
||||
let security_config = SecurityConfig {
|
||||
api_key: "test-key".to_owned(),
|
||||
..SecurityConfig::default()
|
||||
};
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = security_config.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::SecurityConfig(security_config)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_system_status() {
|
||||
let system_status = SystemStatus {
|
||||
version: "1.0.0".to_owned(),
|
||||
start_time: Utc::now(),
|
||||
};
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = system_status.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::SystemStatus(system_status)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_tags() {
|
||||
let tags = vec![Tag {
|
||||
id: 1,
|
||||
label: "rock".to_owned(),
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = tags.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Tags(tags));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_add_artist_search_results() {
|
||||
let search_results = vec![AddArtistSearchResult {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
..AddArtistSearchResult::default()
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = search_results.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::AddArtistSearchResults(search_results)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_albums() {
|
||||
let albums = vec![Album {
|
||||
id: 1,
|
||||
..Album::default()
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = albums.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Albums(albums));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_album() {
|
||||
let album = Album {
|
||||
id: 1,
|
||||
..Album::default()
|
||||
};
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = album.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Album(album));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_status_display() {
|
||||
assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing");
|
||||
assert_str_eq!(ArtistStatus::Ended.to_string(), "ended");
|
||||
assert_str_eq!(ArtistStatus::Deleted.to_string(), "deleted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_status_to_display_str() {
|
||||
assert_str_eq!(ArtistStatus::Continuing.to_display_str(), "Continuing");
|
||||
assert_str_eq!(ArtistStatus::Ended.to_display_str(), "Ended");
|
||||
assert_str_eq!(ArtistStatus::Deleted.to_display_str(), "Deleted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_status_display() {
|
||||
assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown");
|
||||
assert_str_eq!(DownloadStatus::Queued.to_string(), "queued");
|
||||
assert_str_eq!(DownloadStatus::Paused.to_string(), "paused");
|
||||
assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading");
|
||||
assert_str_eq!(DownloadStatus::Completed.to_string(), "completed");
|
||||
assert_str_eq!(DownloadStatus::Failed.to_string(), "failed");
|
||||
assert_str_eq!(DownloadStatus::Warning.to_string(), "warning");
|
||||
assert_str_eq!(DownloadStatus::Delay.to_string(), "delay");
|
||||
assert_str_eq!(
|
||||
DownloadStatus::DownloadClientUnavailable.to_string(),
|
||||
"downloadClientUnavailable"
|
||||
);
|
||||
assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_status_to_display_str() {
|
||||
assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown");
|
||||
assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued");
|
||||
assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused");
|
||||
assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading");
|
||||
assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed");
|
||||
assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed");
|
||||
assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning");
|
||||
assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay");
|
||||
assert_str_eq!(
|
||||
DownloadStatus::DownloadClientUnavailable.to_display_str(),
|
||||
"Download Client Unavailable"
|
||||
);
|
||||
assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_search_result_deserialization() {
|
||||
let search_result_json = json!({
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"artistName": "Test Artist",
|
||||
"status": "continuing",
|
||||
"overview": "Test overview",
|
||||
"artistType": "Group",
|
||||
"disambiguation": "UK Band",
|
||||
"genres": ["Rock", "Alternative"],
|
||||
"ratings": {
|
||||
"votes": 100,
|
||||
"value": 4.5
|
||||
}
|
||||
});
|
||||
|
||||
let search_result: AddArtistSearchResult = serde_json::from_value(search_result_json).unwrap();
|
||||
|
||||
assert_str_eq!(search_result.foreign_artist_id, "test-foreign-id");
|
||||
assert_str_eq!(search_result.artist_name.text, "Test Artist");
|
||||
assert_eq!(search_result.status, ArtistStatus::Continuing);
|
||||
assert_some_eq_x!(&search_result.overview, "Test overview");
|
||||
assert_some_eq_x!(&search_result.artist_type, "Group");
|
||||
assert_some_eq_x!(&search_result.disambiguation, "UK Band");
|
||||
assert_eq!(search_result.genres, vec!["Rock", "Alternative"]);
|
||||
assert_some!(&search_result.ratings);
|
||||
|
||||
let ratings = search_result.ratings.unwrap();
|
||||
assert_eq!(ratings.votes, 100);
|
||||
assert_eq!(ratings.value, 4.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_search_result_with_optional_fields_none() {
|
||||
let search_result_json = json!({
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"artistName": "Test Artist",
|
||||
"status": "ended",
|
||||
"genres": []
|
||||
});
|
||||
|
||||
let search_result: AddArtistSearchResult = serde_json::from_value(search_result_json).unwrap();
|
||||
|
||||
assert_str_eq!(search_result.foreign_artist_id, "test-foreign-id");
|
||||
assert_str_eq!(search_result.artist_name.text, "Test Artist");
|
||||
assert_eq!(search_result.status, ArtistStatus::Ended);
|
||||
assert_none!(&search_result.overview);
|
||||
assert_none!(&search_result.artist_type);
|
||||
assert_none!(&search_result.disambiguation);
|
||||
assert!(search_result.genres.is_empty());
|
||||
assert_none!(&search_result.ratings);
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -3,7 +3,9 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::app::ServarrConfig;
|
||||
use crate::app::context_clues::ContextClue;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use lidarr_models::LidarrSerdeable;
|
||||
use radarr_models::RadarrSerdeable;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
@@ -11,6 +13,7 @@ use serde_json::Number;
|
||||
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use sonarr_models::SonarrSerdeable;
|
||||
|
||||
pub mod lidarr_models;
|
||||
pub mod radarr_models;
|
||||
pub mod servarr_data;
|
||||
pub mod servarr_models;
|
||||
@@ -30,7 +33,7 @@ pub enum Route {
|
||||
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
|
||||
Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
|
||||
Readarr,
|
||||
Lidarr,
|
||||
Lidarr(ActiveLidarrBlock, Option<ActiveLidarrBlock>),
|
||||
Whisparr,
|
||||
Bazarr,
|
||||
Prowlarr,
|
||||
@@ -43,6 +46,7 @@ pub enum Route {
|
||||
pub enum Serdeable {
|
||||
Radarr(RadarrSerdeable),
|
||||
Sonarr(SonarrSerdeable),
|
||||
Lidarr(LidarrSerdeable),
|
||||
}
|
||||
|
||||
pub trait Scrollable {
|
||||
@@ -289,8 +293,7 @@ impl TabState {
|
||||
TabState { tabs, index: 0 }
|
||||
}
|
||||
|
||||
// Allowing this code for now since we'll eventually be implementing additional Servarr support, and we'll need it then
|
||||
#[allow(dead_code)]
|
||||
#[cfg(test)]
|
||||
pub fn set_index(&mut self, index: usize) -> &TabRoute {
|
||||
self.index = index;
|
||||
&self.tabs[self.index]
|
||||
|
||||
@@ -27,7 +27,7 @@ pub struct AddMovieBody {
|
||||
pub title: String,
|
||||
pub root_folder_path: String,
|
||||
pub quality_profile_id: i64,
|
||||
pub minimum_availability: String,
|
||||
pub minimum_availability: MinimumAvailability,
|
||||
pub monitored: bool,
|
||||
pub tags: Vec<i64>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
@@ -55,7 +55,7 @@ pub struct AddMovieSearchResult {
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddMovieOptions {
|
||||
pub monitor: String,
|
||||
pub monitor: MovieMonitor,
|
||||
pub search_for_movie: bool,
|
||||
}
|
||||
|
||||
@@ -268,8 +268,20 @@ pub enum MinimumAvailability {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum, Display, EnumDisplayStyle,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
EnumIter,
|
||||
ValueEnum,
|
||||
Display,
|
||||
EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum MovieMonitor {
|
||||
#[default]
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
use serde_json::Number;
|
||||
|
||||
use super::modals::{AddArtistModal, EditArtistModal};
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::{
|
||||
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
|
||||
lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord},
|
||||
servarr_models::{DiskSpace, RootFolder},
|
||||
stateful_table::StatefulTable,
|
||||
};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use bimap::BiMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use itertools::Itertools;
|
||||
use strum::EnumIter;
|
||||
#[cfg(test)]
|
||||
use {
|
||||
crate::models::lidarr_models::{MonitorType, NewItemMonitorType},
|
||||
crate::models::stateful_table::SortOption,
|
||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map,
|
||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
add_artist_search_result, album, artist, download_record, metadata_profile,
|
||||
metadata_profile_map, quality_profile, root_folder, tags_map,
|
||||
},
|
||||
crate::network::servarr_test_utils::diskspace,
|
||||
strum::{Display, EnumString, IntoEnumIterator},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_data_tests.rs"]
|
||||
mod lidarr_data_tests;
|
||||
|
||||
pub struct LidarrData<'a> {
|
||||
pub add_artist_modal: Option<AddArtistModal>,
|
||||
pub add_artist_search: Option<HorizontallyScrollableText>,
|
||||
pub add_import_list_exclusion: bool,
|
||||
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
|
||||
pub albums: StatefulTable<Album>,
|
||||
pub artist_info_tabs: TabState,
|
||||
pub artists: StatefulTable<Artist>,
|
||||
pub delete_files: bool,
|
||||
pub disk_space_vec: Vec<DiskSpace>,
|
||||
pub downloads: StatefulTable<DownloadRecord>,
|
||||
pub edit_artist_modal: Option<EditArtistModal>,
|
||||
pub main_tabs: TabState,
|
||||
pub metadata_profile_map: BiMap<i64, String>,
|
||||
pub prompt_confirm: bool,
|
||||
pub prompt_confirm_action: Option<LidarrEvent>,
|
||||
pub quality_profile_map: BiMap<i64, String>,
|
||||
pub root_folders: StatefulTable<RootFolder>,
|
||||
pub selected_block: BlockSelectionState<'a, ActiveLidarrBlock>,
|
||||
pub start_time: DateTime<Utc>,
|
||||
pub tags_map: BiMap<i64, String>,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl LidarrData<'_> {
|
||||
pub fn reset_delete_preferences(&mut self) {
|
||||
self.delete_files = false;
|
||||
self.add_import_list_exclusion = false;
|
||||
}
|
||||
|
||||
pub fn reset_artist_info_tabs(&mut self) {
|
||||
self.albums = StatefulTable::default();
|
||||
self.artist_info_tabs.index = 0;
|
||||
}
|
||||
|
||||
pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String {
|
||||
tag_ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let id = id.as_i64()?;
|
||||
self.tags_map.get_by_left(&id).cloned()
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
pub fn sorted_quality_profile_names(&self) -> Vec<String> {
|
||||
self
|
||||
.quality_profile_map
|
||||
.iter()
|
||||
.sorted_by_key(|(id, _)| *id)
|
||||
.map(|(_, name)| name)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn sorted_metadata_profile_names(&self) -> Vec<String> {
|
||||
self
|
||||
.metadata_profile_map
|
||||
.iter()
|
||||
.sorted_by_key(|(id, _)| *id)
|
||||
.map(|(_, name)| name)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Default for LidarrData<'a> {
|
||||
fn default() -> LidarrData<'a> {
|
||||
LidarrData {
|
||||
add_artist_modal: None,
|
||||
add_artist_search: None,
|
||||
add_import_list_exclusion: false,
|
||||
add_searched_artists: None,
|
||||
albums: StatefulTable::default(),
|
||||
artists: StatefulTable::default(),
|
||||
delete_files: false,
|
||||
disk_space_vec: Vec::new(),
|
||||
downloads: StatefulTable::default(),
|
||||
edit_artist_modal: None,
|
||||
metadata_profile_map: BiMap::new(),
|
||||
prompt_confirm: false,
|
||||
prompt_confirm_action: None,
|
||||
quality_profile_map: BiMap::new(),
|
||||
root_folders: StatefulTable::default(),
|
||||
selected_block: BlockSelectionState::default(),
|
||||
start_time: DateTime::default(),
|
||||
tags_map: BiMap::new(),
|
||||
version: String::new(),
|
||||
main_tabs: TabState::new(vec![TabRoute {
|
||||
title: "Library".to_string(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
|
||||
config: None,
|
||||
}]),
|
||||
artist_info_tabs: TabState::new(vec![TabRoute {
|
||||
title: "Albums".to_string(),
|
||||
route: ActiveLidarrBlock::ArtistDetails.into(),
|
||||
contextual_help: Some(&ARTIST_DETAILS_CONTEXT_CLUES),
|
||||
config: None,
|
||||
}]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl LidarrData<'_> {
|
||||
pub fn test_default_fully_populated() -> Self {
|
||||
let mut add_artist_modal = AddArtistModal {
|
||||
tags: "usenet, testing".into(),
|
||||
..AddArtistModal::default()
|
||||
};
|
||||
add_artist_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MonitorType::iter()));
|
||||
add_artist_modal
|
||||
.monitor_new_items_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
add_artist_modal
|
||||
.metadata_profile_list
|
||||
.set_items(vec![metadata_profile().name]);
|
||||
add_artist_modal
|
||||
.quality_profile_list
|
||||
.set_items(vec![quality_profile().name]);
|
||||
add_artist_modal
|
||||
.root_folder_list
|
||||
.set_items(vec![root_folder()]);
|
||||
let mut edit_artist_modal = EditArtistModal {
|
||||
monitored: Some(true),
|
||||
path: "/nfs/music".into(),
|
||||
tags: "alex".into(),
|
||||
..EditArtistModal::default()
|
||||
};
|
||||
edit_artist_modal
|
||||
.monitor_list
|
||||
.set_items(NewItemMonitorType::iter().collect());
|
||||
edit_artist_modal
|
||||
.quality_profile_list
|
||||
.set_items(vec![quality_profile().name]);
|
||||
edit_artist_modal
|
||||
.metadata_profile_list
|
||||
.set_items(vec![metadata_profile().name]);
|
||||
|
||||
let mut lidarr_data = LidarrData {
|
||||
delete_files: true,
|
||||
disk_space_vec: vec![diskspace()],
|
||||
quality_profile_map: quality_profile_map(),
|
||||
metadata_profile_map: metadata_profile_map(),
|
||||
edit_artist_modal: Some(edit_artist_modal),
|
||||
add_artist_modal: Some(add_artist_modal),
|
||||
tags_map: tags_map(),
|
||||
..LidarrData::default()
|
||||
};
|
||||
lidarr_data.albums.set_items(vec![album()]);
|
||||
lidarr_data.albums.search = Some("album search".into());
|
||||
lidarr_data.artists.set_items(vec![artist()]);
|
||||
lidarr_data.artists.sorting(vec![SortOption {
|
||||
name: "Name",
|
||||
cmp_fn: Some(|a: &Artist, b: &Artist| a.artist_name.text.cmp(&b.artist_name.text)),
|
||||
}]);
|
||||
lidarr_data.artists.search = Some("artist search".into());
|
||||
lidarr_data.artists.filter = Some("artist filter".into());
|
||||
lidarr_data.downloads.set_items(vec![download_record()]);
|
||||
lidarr_data.root_folders.set_items(vec![root_folder()]);
|
||||
lidarr_data.version = "1.0.0".to_owned();
|
||||
lidarr_data.add_artist_search = Some("Test Artist".into());
|
||||
let mut add_searched_artists = StatefulTable::default();
|
||||
add_searched_artists.set_items(vec![add_artist_search_result()]);
|
||||
lidarr_data.add_searched_artists = Some(add_searched_artists);
|
||||
|
||||
lidarr_data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)]
|
||||
#[cfg_attr(test, derive(Display, EnumString))]
|
||||
pub enum ActiveLidarrBlock {
|
||||
#[default]
|
||||
Artists,
|
||||
ArtistDetails,
|
||||
ArtistsSortPrompt,
|
||||
AddArtistAlreadyInLibrary,
|
||||
AddArtistConfirmPrompt,
|
||||
AddArtistEmptySearchResults,
|
||||
AddArtistPrompt,
|
||||
AddArtistSearchInput,
|
||||
AddArtistSearchResults,
|
||||
AddArtistSelectMetadataProfile,
|
||||
AddArtistSelectMonitor,
|
||||
AddArtistSelectMonitorNewItems,
|
||||
AddArtistSelectQualityProfile,
|
||||
AddArtistSelectRootFolder,
|
||||
AddArtistTagsInput,
|
||||
AutomaticallySearchArtistPrompt,
|
||||
DeleteAlbumPrompt,
|
||||
DeleteAlbumConfirmPrompt,
|
||||
DeleteAlbumToggleDeleteFile,
|
||||
DeleteAlbumToggleAddListExclusion,
|
||||
DeleteArtistPrompt,
|
||||
DeleteArtistConfirmPrompt,
|
||||
DeleteArtistToggleDeleteFile,
|
||||
DeleteArtistToggleAddListExclusion,
|
||||
EditArtistPrompt,
|
||||
EditArtistConfirmPrompt,
|
||||
EditArtistPathInput,
|
||||
EditArtistSelectMetadataProfile,
|
||||
EditArtistSelectMonitorNewItems,
|
||||
EditArtistSelectQualityProfile,
|
||||
EditArtistTagsInput,
|
||||
EditArtistToggleMonitored,
|
||||
FilterArtists,
|
||||
FilterArtistsError,
|
||||
SearchAlbums,
|
||||
SearchAlbumsError,
|
||||
SearchArtists,
|
||||
SearchArtistsError,
|
||||
UpdateAllArtistsPrompt,
|
||||
UpdateAndScanArtistPrompt,
|
||||
}
|
||||
|
||||
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
|
||||
ActiveLidarrBlock::Artists,
|
||||
ActiveLidarrBlock::ArtistsSortPrompt,
|
||||
ActiveLidarrBlock::FilterArtists,
|
||||
ActiveLidarrBlock::FilterArtistsError,
|
||||
ActiveLidarrBlock::SearchArtists,
|
||||
ActiveLidarrBlock::SearchArtistsError,
|
||||
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||
];
|
||||
|
||||
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
|
||||
ActiveLidarrBlock::SearchAlbums,
|
||||
ActiveLidarrBlock::SearchAlbumsError,
|
||||
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
|
||||
];
|
||||
|
||||
pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 12] = [
|
||||
ActiveLidarrBlock::AddArtistAlreadyInLibrary,
|
||||
ActiveLidarrBlock::AddArtistConfirmPrompt,
|
||||
ActiveLidarrBlock::AddArtistEmptySearchResults,
|
||||
ActiveLidarrBlock::AddArtistPrompt,
|
||||
ActiveLidarrBlock::AddArtistSearchInput,
|
||||
ActiveLidarrBlock::AddArtistSearchResults,
|
||||
ActiveLidarrBlock::AddArtistSelectMetadataProfile,
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor,
|
||||
ActiveLidarrBlock::AddArtistSelectMonitorNewItems,
|
||||
ActiveLidarrBlock::AddArtistSelectQualityProfile,
|
||||
ActiveLidarrBlock::AddArtistSelectRootFolder,
|
||||
ActiveLidarrBlock::AddArtistTagsInput,
|
||||
];
|
||||
|
||||
pub const ADD_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
|
||||
&[ActiveLidarrBlock::AddArtistSelectRootFolder],
|
||||
&[ActiveLidarrBlock::AddArtistSelectMonitor],
|
||||
&[ActiveLidarrBlock::AddArtistSelectMonitorNewItems],
|
||||
&[ActiveLidarrBlock::AddArtistSelectQualityProfile],
|
||||
&[ActiveLidarrBlock::AddArtistSelectMetadataProfile],
|
||||
&[ActiveLidarrBlock::AddArtistTagsInput],
|
||||
&[ActiveLidarrBlock::AddArtistConfirmPrompt],
|
||||
];
|
||||
|
||||
pub static DELETE_ARTIST_BLOCKS: [ActiveLidarrBlock; 4] = [
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistConfirmPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistToggleDeleteFile,
|
||||
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion,
|
||||
];
|
||||
|
||||
pub const DELETE_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
|
||||
&[ActiveLidarrBlock::DeleteArtistToggleDeleteFile],
|
||||
&[ActiveLidarrBlock::DeleteArtistToggleAddListExclusion],
|
||||
&[ActiveLidarrBlock::DeleteArtistConfirmPrompt],
|
||||
];
|
||||
|
||||
pub static DELETE_ALBUM_BLOCKS: [ActiveLidarrBlock; 4] = [
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
ActiveLidarrBlock::DeleteAlbumConfirmPrompt,
|
||||
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile,
|
||||
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion,
|
||||
];
|
||||
|
||||
pub const DELETE_ALBUM_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
|
||||
&[ActiveLidarrBlock::DeleteAlbumToggleDeleteFile],
|
||||
&[ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion],
|
||||
&[ActiveLidarrBlock::DeleteAlbumConfirmPrompt],
|
||||
];
|
||||
|
||||
pub static EDIT_ARTIST_BLOCKS: [ActiveLidarrBlock; 8] = [
|
||||
ActiveLidarrBlock::EditArtistPrompt,
|
||||
ActiveLidarrBlock::EditArtistConfirmPrompt,
|
||||
ActiveLidarrBlock::EditArtistPathInput,
|
||||
ActiveLidarrBlock::EditArtistSelectMetadataProfile,
|
||||
ActiveLidarrBlock::EditArtistSelectMonitorNewItems,
|
||||
ActiveLidarrBlock::EditArtistSelectQualityProfile,
|
||||
ActiveLidarrBlock::EditArtistTagsInput,
|
||||
ActiveLidarrBlock::EditArtistToggleMonitored,
|
||||
];
|
||||
|
||||
pub const EDIT_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
|
||||
&[ActiveLidarrBlock::EditArtistToggleMonitored],
|
||||
&[ActiveLidarrBlock::EditArtistSelectMonitorNewItems],
|
||||
&[ActiveLidarrBlock::EditArtistSelectQualityProfile],
|
||||
&[ActiveLidarrBlock::EditArtistSelectMetadataProfile],
|
||||
&[ActiveLidarrBlock::EditArtistPathInput],
|
||||
&[ActiveLidarrBlock::EditArtistTagsInput],
|
||||
&[ActiveLidarrBlock::EditArtistConfirmPrompt],
|
||||
];
|
||||
|
||||
impl From<ActiveLidarrBlock> for Route {
|
||||
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
|
||||
Route::Lidarr(active_lidarr_block, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(ActiveLidarrBlock, Option<ActiveLidarrBlock>)> for Route {
|
||||
fn from(value: (ActiveLidarrBlock, Option<ActiveLidarrBlock>)) -> Route {
|
||||
Route::Lidarr(value.0, value.1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::lidarr_models::Album;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS,
|
||||
DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
|
||||
EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::{
|
||||
BlockSelectionState, Route,
|
||||
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS, LidarrData},
|
||||
};
|
||||
use bimap::BiMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use serde_json::Number;
|
||||
|
||||
#[test]
|
||||
fn test_from_active_lidarr_block_to_route() {
|
||||
assert_eq!(
|
||||
Route::from(ActiveLidarrBlock::Artists),
|
||||
Route::Lidarr(ActiveLidarrBlock::Artists, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_tuple_to_route_with_context() {
|
||||
assert_eq!(
|
||||
Route::from((ActiveLidarrBlock::Artists, Some(ActiveLidarrBlock::Artists))),
|
||||
Route::Lidarr(ActiveLidarrBlock::Artists, Some(ActiveLidarrBlock::Artists),)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_delete_preferences() {
|
||||
let mut lidarr_data = LidarrData {
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
..LidarrData::default()
|
||||
};
|
||||
|
||||
lidarr_data.reset_delete_preferences();
|
||||
|
||||
assert!(!lidarr_data.delete_files);
|
||||
assert!(!lidarr_data.add_import_list_exclusion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_artist_info_tabs() {
|
||||
let mut lidarr_data = LidarrData::default();
|
||||
lidarr_data.albums.set_items(vec![Album::default()]);
|
||||
lidarr_data.artist_info_tabs.index = 1;
|
||||
|
||||
lidarr_data.reset_artist_info_tabs();
|
||||
|
||||
assert_is_empty!(lidarr_data.albums);
|
||||
assert_eq!(lidarr_data.artist_info_tabs.index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag_ids_to_display() {
|
||||
let mut tags_map = BiMap::new();
|
||||
tags_map.insert(3, "test 3".to_owned());
|
||||
tags_map.insert(2, "test 2".to_owned());
|
||||
tags_map.insert(1, "test 1".to_owned());
|
||||
let lidarr_data = LidarrData {
|
||||
tags_map,
|
||||
..LidarrData::default()
|
||||
};
|
||||
|
||||
assert_str_eq!(
|
||||
lidarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]),
|
||||
"test 1, test 2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorted_quality_profile_names() {
|
||||
let mut quality_profile_map = BiMap::new();
|
||||
quality_profile_map.insert(3, "test 1".to_owned());
|
||||
quality_profile_map.insert(2, "test 2".to_owned());
|
||||
quality_profile_map.insert(1, "test 3".to_owned());
|
||||
let lidarr_data = LidarrData {
|
||||
quality_profile_map,
|
||||
..LidarrData::default()
|
||||
};
|
||||
let expected_quality_profile_vec = vec![
|
||||
"test 3".to_owned(),
|
||||
"test 2".to_owned(),
|
||||
"test 1".to_owned(),
|
||||
];
|
||||
|
||||
assert_iter_eq!(
|
||||
lidarr_data.sorted_quality_profile_names(),
|
||||
expected_quality_profile_vec
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorted_metadata_profile_names() {
|
||||
let mut metadata_profile_map = BiMap::new();
|
||||
metadata_profile_map.insert(3, "test 1".to_owned());
|
||||
metadata_profile_map.insert(2, "test 2".to_owned());
|
||||
metadata_profile_map.insert(1, "test 3".to_owned());
|
||||
let lidarr_data = LidarrData {
|
||||
metadata_profile_map,
|
||||
..LidarrData::default()
|
||||
};
|
||||
let expected_metadata_profile_vec = vec![
|
||||
"test 3".to_owned(),
|
||||
"test 2".to_owned(),
|
||||
"test 1".to_owned(),
|
||||
];
|
||||
|
||||
assert_iter_eq!(
|
||||
lidarr_data.sorted_metadata_profile_names(),
|
||||
expected_metadata_profile_vec
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_data_default() {
|
||||
let lidarr_data = LidarrData::default();
|
||||
|
||||
assert_none!(lidarr_data.add_artist_search);
|
||||
assert!(!lidarr_data.add_import_list_exclusion);
|
||||
assert_none!(lidarr_data.add_searched_artists);
|
||||
assert_is_empty!(lidarr_data.albums);
|
||||
assert_is_empty!(lidarr_data.artists);
|
||||
assert!(!lidarr_data.delete_files);
|
||||
assert_is_empty!(lidarr_data.disk_space_vec);
|
||||
assert_is_empty!(lidarr_data.downloads);
|
||||
assert_none!(lidarr_data.edit_artist_modal);
|
||||
assert_is_empty!(lidarr_data.metadata_profile_map);
|
||||
assert!(!lidarr_data.prompt_confirm);
|
||||
assert_none!(lidarr_data.prompt_confirm_action);
|
||||
assert_is_empty!(lidarr_data.quality_profile_map);
|
||||
assert_is_empty!(lidarr_data.root_folders);
|
||||
assert_eq!(lidarr_data.selected_block, BlockSelectionState::default());
|
||||
assert_eq!(lidarr_data.start_time, <DateTime<Utc>>::default());
|
||||
assert_is_empty!(lidarr_data.tags_map);
|
||||
assert_is_empty!(lidarr_data.version);
|
||||
|
||||
assert_eq!(lidarr_data.main_tabs.tabs.len(), 1);
|
||||
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library");
|
||||
assert_eq!(
|
||||
lidarr_data.main_tabs.tabs[0].route,
|
||||
ActiveLidarrBlock::Artists.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&lidarr_data.main_tabs.tabs[0].contextual_help,
|
||||
&ARTISTS_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.main_tabs.tabs[0].config);
|
||||
|
||||
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1);
|
||||
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
|
||||
assert_eq!(
|
||||
lidarr_data.artist_info_tabs.tabs[0].route,
|
||||
ActiveLidarrBlock::ArtistDetails.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&lidarr_data.artist_info_tabs.tabs[0].contextual_help,
|
||||
&ARTIST_DETAILS_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.artist_info_tabs.tabs[0].config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_blocks_contains_expected_blocks() {
|
||||
assert_eq!(LIBRARY_BLOCKS.len(), 7);
|
||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::Artists));
|
||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::ArtistsSortPrompt));
|
||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::SearchArtists));
|
||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistsError));
|
||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::FilterArtists));
|
||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistsError));
|
||||
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::UpdateAllArtistsPrompt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_blocks_contains_expected_blocks() {
|
||||
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 5);
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistDetails));
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchArtistPrompt));
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbums));
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumsError));
|
||||
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_blocks_contents() {
|
||||
assert_eq!(ADD_ARTIST_BLOCKS.len(), 12);
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistAlreadyInLibrary));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistConfirmPrompt));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistEmptySearchResults));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistPrompt));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchInput));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchResults));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMetadataProfile));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMonitor));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMonitorNewItems));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectQualityProfile));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectRootFolder));
|
||||
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistTagsInput));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_selection_blocks_ordering() {
|
||||
let mut add_artist_block_iter = ADD_ARTIST_SELECTION_BLOCKS.iter();
|
||||
|
||||
assert_eq!(
|
||||
add_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::AddArtistSelectRootFolder]
|
||||
);
|
||||
assert_eq!(
|
||||
add_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::AddArtistSelectMonitor]
|
||||
);
|
||||
assert_eq!(
|
||||
add_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::AddArtistSelectMonitorNewItems]
|
||||
);
|
||||
assert_eq!(
|
||||
add_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::AddArtistSelectQualityProfile]
|
||||
);
|
||||
assert_eq!(
|
||||
add_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::AddArtistSelectMetadataProfile]
|
||||
);
|
||||
assert_eq!(
|
||||
add_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::AddArtistTagsInput]
|
||||
);
|
||||
assert_eq!(
|
||||
add_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::AddArtistConfirmPrompt]
|
||||
);
|
||||
assert_none!(add_artist_block_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_blocks_contents() {
|
||||
assert_eq!(DELETE_ARTIST_BLOCKS.len(), 4);
|
||||
assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistPrompt));
|
||||
assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistConfirmPrompt));
|
||||
assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistToggleDeleteFile));
|
||||
assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistToggleAddListExclusion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_selection_blocks_ordering() {
|
||||
let mut delete_artist_block_iter = DELETE_ARTIST_SELECTION_BLOCKS.iter();
|
||||
|
||||
assert_eq!(
|
||||
delete_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::DeleteArtistToggleDeleteFile]
|
||||
);
|
||||
assert_eq!(
|
||||
delete_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::DeleteArtistToggleAddListExclusion]
|
||||
);
|
||||
assert_eq!(
|
||||
delete_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::DeleteArtistConfirmPrompt]
|
||||
);
|
||||
assert_none!(delete_artist_block_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_blocks_contents() {
|
||||
assert_eq!(DELETE_ALBUM_BLOCKS.len(), 4);
|
||||
assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumPrompt));
|
||||
assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumConfirmPrompt));
|
||||
assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumToggleDeleteFile));
|
||||
assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_selection_blocks_ordering() {
|
||||
let mut delete_album_block_iter = DELETE_ALBUM_SELECTION_BLOCKS.iter();
|
||||
|
||||
assert_eq!(
|
||||
delete_album_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::DeleteAlbumToggleDeleteFile]
|
||||
);
|
||||
assert_eq!(
|
||||
delete_album_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion]
|
||||
);
|
||||
assert_eq!(
|
||||
delete_album_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::DeleteAlbumConfirmPrompt]
|
||||
);
|
||||
assert_none!(delete_album_block_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_blocks() {
|
||||
assert_eq!(EDIT_ARTIST_BLOCKS.len(), 8);
|
||||
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistPrompt));
|
||||
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistConfirmPrompt));
|
||||
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistPathInput));
|
||||
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectMetadataProfile));
|
||||
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectMonitorNewItems));
|
||||
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectQualityProfile));
|
||||
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistTagsInput));
|
||||
assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistToggleMonitored));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_selection_blocks_ordering() {
|
||||
let mut edit_artist_block_iter = EDIT_ARTIST_SELECTION_BLOCKS.iter();
|
||||
|
||||
assert_eq!(
|
||||
edit_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::EditArtistToggleMonitored]
|
||||
);
|
||||
assert_eq!(
|
||||
edit_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::EditArtistSelectMonitorNewItems]
|
||||
);
|
||||
assert_eq!(
|
||||
edit_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::EditArtistSelectQualityProfile]
|
||||
);
|
||||
assert_eq!(
|
||||
edit_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::EditArtistSelectMetadataProfile]
|
||||
);
|
||||
assert_eq!(
|
||||
edit_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::EditArtistPathInput]
|
||||
);
|
||||
assert_eq!(
|
||||
edit_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::EditArtistTagsInput]
|
||||
);
|
||||
assert_eq!(
|
||||
edit_artist_block_iter.next().unwrap(),
|
||||
&[ActiveLidarrBlock::EditArtistConfirmPrompt]
|
||||
);
|
||||
assert_none!(edit_artist_block_iter.next());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod lidarr_data;
|
||||
pub mod modals;
|
||||
@@ -0,0 +1,115 @@
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use super::lidarr_data::LidarrData;
|
||||
use crate::models::{
|
||||
HorizontallyScrollableText,
|
||||
lidarr_models::{MonitorType, NewItemMonitorType},
|
||||
servarr_models::RootFolder,
|
||||
stateful_list::StatefulList,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "modals_tests.rs"]
|
||||
mod modals_tests;
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub struct AddArtistModal {
|
||||
pub root_folder_list: StatefulList<RootFolder>,
|
||||
pub monitor_list: StatefulList<MonitorType>,
|
||||
pub monitor_new_items_list: StatefulList<NewItemMonitorType>,
|
||||
pub quality_profile_list: StatefulList<String>,
|
||||
pub metadata_profile_list: StatefulList<String>,
|
||||
pub tags: HorizontallyScrollableText,
|
||||
}
|
||||
|
||||
impl From<&LidarrData<'_>> for AddArtistModal {
|
||||
fn from(lidarr_data: &LidarrData<'_>) -> AddArtistModal {
|
||||
let mut add_artist_modal = AddArtistModal::default();
|
||||
add_artist_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MonitorType::iter()));
|
||||
add_artist_modal
|
||||
.monitor_new_items_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
add_artist_modal
|
||||
.quality_profile_list
|
||||
.set_items(lidarr_data.sorted_quality_profile_names());
|
||||
add_artist_modal
|
||||
.metadata_profile_list
|
||||
.set_items(lidarr_data.sorted_metadata_profile_names());
|
||||
add_artist_modal
|
||||
.root_folder_list
|
||||
.set_items(lidarr_data.root_folders.items.to_vec());
|
||||
|
||||
add_artist_modal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub struct EditArtistModal {
|
||||
pub monitor_list: StatefulList<NewItemMonitorType>,
|
||||
pub quality_profile_list: StatefulList<String>,
|
||||
pub metadata_profile_list: StatefulList<String>,
|
||||
pub monitored: Option<bool>,
|
||||
pub path: HorizontallyScrollableText,
|
||||
pub tags: HorizontallyScrollableText,
|
||||
}
|
||||
|
||||
impl From<&LidarrData<'_>> for EditArtistModal {
|
||||
fn from(lidarr_data: &LidarrData<'_>) -> EditArtistModal {
|
||||
let mut edit_artist_modal = EditArtistModal::default();
|
||||
let artist = lidarr_data.artists.current_selection();
|
||||
|
||||
edit_artist_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
edit_artist_modal.path = artist.path.clone().into();
|
||||
edit_artist_modal.tags = lidarr_data.tag_ids_to_display(&artist.tags).into();
|
||||
edit_artist_modal.monitored = Some(artist.monitored);
|
||||
|
||||
let monitor_index = edit_artist_modal
|
||||
.monitor_list
|
||||
.items
|
||||
.iter()
|
||||
.position(|m| *m == artist.monitor_new_items);
|
||||
edit_artist_modal.monitor_list.state.select(monitor_index);
|
||||
|
||||
edit_artist_modal
|
||||
.quality_profile_list
|
||||
.set_items(lidarr_data.sorted_quality_profile_names());
|
||||
let quality_profile_name = lidarr_data
|
||||
.quality_profile_map
|
||||
.get_by_left(&artist.quality_profile_id)
|
||||
.unwrap();
|
||||
let quality_profile_index = edit_artist_modal
|
||||
.quality_profile_list
|
||||
.items
|
||||
.iter()
|
||||
.position(|profile| profile == quality_profile_name);
|
||||
edit_artist_modal
|
||||
.quality_profile_list
|
||||
.state
|
||||
.select(quality_profile_index);
|
||||
|
||||
edit_artist_modal
|
||||
.metadata_profile_list
|
||||
.set_items(lidarr_data.sorted_metadata_profile_names());
|
||||
let metadata_profile_name = lidarr_data
|
||||
.metadata_profile_map
|
||||
.get_by_left(&artist.metadata_profile_id)
|
||||
.unwrap();
|
||||
let metadata_profile_index = edit_artist_modal
|
||||
.metadata_profile_list
|
||||
.items
|
||||
.iter()
|
||||
.position(|profile| profile == metadata_profile_name);
|
||||
edit_artist_modal
|
||||
.metadata_profile_list
|
||||
.state
|
||||
.select(metadata_profile_index);
|
||||
|
||||
edit_artist_modal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
|
||||
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_models::RootFolder;
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_modal_from_lidarr_data() {
|
||||
let mut lidarr_data = LidarrData {
|
||||
quality_profile_map: BiMap::from_iter([
|
||||
(2i64, "Lossless".to_owned()),
|
||||
(1i64, "Standard".to_owned()),
|
||||
]),
|
||||
metadata_profile_map: BiMap::from_iter([
|
||||
(2i64, "None".to_owned()),
|
||||
(1i64, "Standard".to_owned()),
|
||||
]),
|
||||
..LidarrData::default()
|
||||
};
|
||||
let root_folder_1 = RootFolder {
|
||||
id: 1,
|
||||
path: "/nfs".to_owned(),
|
||||
accessible: true,
|
||||
free_space: 219902325555200,
|
||||
unmapped_folders: None,
|
||||
};
|
||||
lidarr_data.root_folders.set_items(vec![
|
||||
root_folder_1.clone(),
|
||||
RootFolder {
|
||||
id: 2,
|
||||
path: "/nfs2".to_owned(),
|
||||
accessible: true,
|
||||
free_space: 21990232555520,
|
||||
unmapped_folders: None,
|
||||
},
|
||||
]);
|
||||
|
||||
let add_artist_modal = AddArtistModal::from(&lidarr_data);
|
||||
|
||||
assert_eq!(
|
||||
*add_artist_modal.monitor_list.current_selection(),
|
||||
MonitorType::default()
|
||||
);
|
||||
assert_eq!(
|
||||
*add_artist_modal.monitor_new_items_list.current_selection(),
|
||||
NewItemMonitorType::default()
|
||||
);
|
||||
assert_str_eq!(
|
||||
add_artist_modal.quality_profile_list.current_selection(),
|
||||
"Standard"
|
||||
);
|
||||
assert_str_eq!(
|
||||
add_artist_modal.metadata_profile_list.current_selection(),
|
||||
"Standard"
|
||||
);
|
||||
assert_eq!(
|
||||
add_artist_modal.root_folder_list.current_selection(),
|
||||
&root_folder_1
|
||||
);
|
||||
assert_is_empty!(add_artist_modal.tags.text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_modal_from_lidarr_data() {
|
||||
let mut lidarr_data = LidarrData {
|
||||
quality_profile_map: BiMap::from_iter([
|
||||
(1i64, "HD - 1080p".to_owned()),
|
||||
(2i64, "Any".to_owned()),
|
||||
]),
|
||||
metadata_profile_map: BiMap::from_iter([
|
||||
(1i64, "Standard".to_owned()),
|
||||
(2i64, "None".to_owned()),
|
||||
]),
|
||||
tags_map: BiMap::from_iter([(1i64, "usenet".to_owned())]),
|
||||
..LidarrData::default()
|
||||
};
|
||||
let artist = Artist {
|
||||
id: 1,
|
||||
monitored: true,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
path: "/nfs/music/test_artist".to_owned(),
|
||||
tags: vec![serde_json::Number::from(1)],
|
||||
..Artist::default()
|
||||
};
|
||||
lidarr_data.artists.set_items(vec![artist]);
|
||||
|
||||
let edit_artist_modal = EditArtistModal::from(&lidarr_data);
|
||||
|
||||
assert_eq!(edit_artist_modal.monitored, Some(true));
|
||||
assert_eq!(
|
||||
*edit_artist_modal.monitor_list.current_selection(),
|
||||
NewItemMonitorType::All
|
||||
);
|
||||
assert_str_eq!(
|
||||
edit_artist_modal.quality_profile_list.current_selection(),
|
||||
"HD - 1080p"
|
||||
);
|
||||
assert_str_eq!(
|
||||
edit_artist_modal.metadata_profile_list.current_selection(),
|
||||
"Standard"
|
||||
);
|
||||
assert_str_eq!(edit_artist_modal.path.text, "/nfs/music/test_artist");
|
||||
assert_str_eq!(edit_artist_modal.tags.text, "usenet");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user