Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4abf705cb5 | |||
| cf98b10d77 | |||
| f0ed71b436 | |||
| 243de47cae | |||
| d3947d9e15 | |||
| 64d8c65831 | |||
| 60c4cf1098 | |||
| 9cc3ccb419 | |||
| 45c61369c8 | |||
| a8609e08c5 | |||
| a18b047f4f | |||
| b1afdaf541 | |||
| 3c1634d1e3 | |||
| 9b4eda6a9d | |||
| 96308afeee | |||
| 4e13d5d34d | |||
| b4a99d1665 | |||
| a012f6ecd5 | |||
| 5afee1998b | |||
| 059fa48bd9 | |||
| 6771a0ab38 | |||
| bc3aeefa6e | |||
| e61537942b | |||
| 5d09b2402c | |||
| 368f7505ff | |||
| 6a9fd0999c | |||
| d8ac94d067 |
Generated
+1037
-397
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -42,7 +42,7 @@ strum = { version = "0.26.3", features = ["derive"] }
|
|||||||
strum_macros = "0.26.4"
|
strum_macros = "0.26.4"
|
||||||
tokio = { version = "1.44.2", features = ["full"] }
|
tokio = { version = "1.44.2", features = ["full"] }
|
||||||
tokio-util = "0.7.8"
|
tokio-util = "0.7.8"
|
||||||
ratatui = { version = "0.29.0", features = [
|
ratatui = { version = "0.30.0", features = [
|
||||||
"all-widgets",
|
"all-widgets",
|
||||||
"unstable-widget-ref",
|
"unstable-widget-ref",
|
||||||
] }
|
] }
|
||||||
@@ -59,7 +59,7 @@ ctrlc = "3.4.5"
|
|||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
dirs-next = "2.0.0"
|
dirs-next = "2.0.0"
|
||||||
managarr-tree-widget = "0.24.0"
|
managarr-tree-widget = "0.25.0"
|
||||||
indicatif = "0.17.9"
|
indicatif = "0.17.9"
|
||||||
derive_setters = "0.1.6"
|
derive_setters = "0.1.6"
|
||||||
deunicode = "1.6.0"
|
deunicode = "1.6.0"
|
||||||
@@ -91,4 +91,6 @@ name = "managarr"
|
|||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 3
|
||||||
|
opt-level = "s"
|
||||||
|
strip = true
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod tests {
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
|
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::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||||
use crate::models::{HorizontallyScrollableText, TabRoute};
|
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||||
@@ -35,6 +36,7 @@ mod tests {
|
|||||||
theme: None,
|
theme: None,
|
||||||
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
||||||
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
||||||
|
lidarr: None,
|
||||||
};
|
};
|
||||||
let expected_tab_routes = vec![
|
let expected_tab_routes = vec![
|
||||||
TabRoute {
|
TabRoute {
|
||||||
@@ -80,6 +82,7 @@ mod tests {
|
|||||||
assert_eq!(app.tick_until_poll, 400);
|
assert_eq!(app.tick_until_poll, 400);
|
||||||
assert_eq!(app.ticks_until_scroll, 4);
|
assert_eq!(app.ticks_until_scroll, 4);
|
||||||
assert_eq!(app.tick_count, 0);
|
assert_eq!(app.tick_count, 0);
|
||||||
|
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||||
assert!(!app.is_loading);
|
assert!(!app.is_loading);
|
||||||
assert!(!app.is_routing);
|
assert!(!app.is_routing);
|
||||||
assert!(!app.should_refresh);
|
assert!(!app.should_refresh);
|
||||||
@@ -183,6 +186,7 @@ mod tests {
|
|||||||
..SonarrData::default()
|
..SonarrData::default()
|
||||||
};
|
};
|
||||||
let data = Data {
|
let data = Data {
|
||||||
|
lidarr_data: LidarrData::default(),
|
||||||
radarr_data,
|
radarr_data,
|
||||||
sonarr_data,
|
sonarr_data,
|
||||||
};
|
};
|
||||||
@@ -240,6 +244,27 @@ mod tests {
|
|||||||
assert_eq!(app.tick_count, 0);
|
assert_eq!(app.tick_count, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_on_ui_scroll_tick() {
|
||||||
|
let mut app = App {
|
||||||
|
ticks_until_scroll: 1,
|
||||||
|
..App::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||||
|
assert_eq!(app.tick_count, 0);
|
||||||
|
|
||||||
|
app.on_ui_scroll_tick();
|
||||||
|
|
||||||
|
assert_eq!(app.ui_scroll_tick_count, 1);
|
||||||
|
assert_eq!(app.tick_count, 0);
|
||||||
|
|
||||||
|
app.on_ui_scroll_tick();
|
||||||
|
|
||||||
|
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||||
|
assert_eq!(app.tick_count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_on_tick_first_render() {
|
async fn test_on_tick_first_render() {
|
||||||
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
|
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
|
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::radarr::radarr_context_clues::RadarrContextClueProvider;
|
||||||
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
|
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
@@ -21,6 +22,7 @@ impl ContextClueProvider for ServarrContextClueProvider {
|
|||||||
match app.get_current_route() {
|
match app.get_current_route() {
|
||||||
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
|
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
|
||||||
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
|
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
|
||||||
|
Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
use crate::app::App;
|
||||||
|
use crate::app::context_clues::{BARE_POPUP_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, ActiveLidarrBlock};
|
||||||
|
|
||||||
|
#[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(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 {
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults => {
|
||||||
|
Some(&BARE_POPUP_CONTEXT_CLUES)
|
||||||
|
}
|
||||||
|
_ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
|
||||||
|
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,172 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::app::context_clues::{
|
||||||
|
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider,
|
||||||
|
};
|
||||||
|
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||||
|
use crate::app::lidarr::lidarr_context_clues::{
|
||||||
|
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
|
||||||
|
};
|
||||||
|
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
|
||||||
|
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_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#[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 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_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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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::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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
-1
@@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use veil::Redact;
|
use veil::Redact;
|
||||||
|
|
||||||
use crate::cli::Command;
|
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::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||||
use crate::models::servarr_models::KeybindingItem;
|
use crate::models::servarr_models::KeybindingItem;
|
||||||
@@ -25,6 +26,7 @@ mod app_tests;
|
|||||||
pub mod context_clues;
|
pub mod context_clues;
|
||||||
pub mod key_binding;
|
pub mod key_binding;
|
||||||
mod key_binding_tests;
|
mod key_binding_tests;
|
||||||
|
pub mod lidarr;
|
||||||
pub mod radarr;
|
pub mod radarr;
|
||||||
pub mod sonarr;
|
pub mod sonarr;
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ pub struct App<'a> {
|
|||||||
pub tick_until_poll: u64,
|
pub tick_until_poll: u64,
|
||||||
pub ticks_until_scroll: u64,
|
pub ticks_until_scroll: u64,
|
||||||
pub tick_count: u64,
|
pub tick_count: u64,
|
||||||
|
pub ui_scroll_tick_count: u64,
|
||||||
pub is_routing: bool,
|
pub is_routing: bool,
|
||||||
pub is_loading: bool,
|
pub is_loading: bool,
|
||||||
pub should_refresh: bool,
|
pub should_refresh: bool,
|
||||||
@@ -95,6 +98,26 @@ impl App<'_> {
|
|||||||
server_tabs.extend(sonarr_tabs);
|
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
|
let weight_sorted_tabs = server_tabs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by(|tab1, tab2| {
|
.sorted_by(|tab1, tab2| {
|
||||||
@@ -145,6 +168,14 @@ impl App<'_> {
|
|||||||
self.tick_count = 0;
|
self.tick_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_ui_scroll_tick(&mut self) {
|
||||||
|
if self.ui_scroll_tick_count == self.ticks_until_scroll {
|
||||||
|
self.ui_scroll_tick_count = 0;
|
||||||
|
} else {
|
||||||
|
self.ui_scroll_tick_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.reset_tick_count();
|
self.reset_tick_count();
|
||||||
@@ -167,6 +198,7 @@ impl App<'_> {
|
|||||||
match self.get_current_route() {
|
match self.get_current_route() {
|
||||||
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
|
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::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,
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +259,7 @@ impl Default for App<'_> {
|
|||||||
tick_until_poll: 400,
|
tick_until_poll: 400,
|
||||||
ticks_until_scroll: 4,
|
ticks_until_scroll: 4,
|
||||||
tick_count: 0,
|
tick_count: 0,
|
||||||
|
ui_scroll_tick_count: 0,
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
is_routing: false,
|
is_routing: false,
|
||||||
should_refresh: false,
|
should_refresh: false,
|
||||||
@@ -254,6 +287,12 @@ impl App<'_> {
|
|||||||
contextual_help: None,
|
contextual_help: None,
|
||||||
config: Some(ServarrConfig::default()),
|
config: Some(ServarrConfig::default()),
|
||||||
},
|
},
|
||||||
|
TabRoute {
|
||||||
|
title: "Lidarr".to_owned(),
|
||||||
|
route: ActiveLidarrBlock::Artists.into(),
|
||||||
|
contextual_help: None,
|
||||||
|
config: Some(ServarrConfig::default()),
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
..App::default()
|
..App::default()
|
||||||
}
|
}
|
||||||
@@ -262,6 +301,7 @@ impl App<'_> {
|
|||||||
pub fn test_default_fully_populated() -> Self {
|
pub fn test_default_fully_populated() -> Self {
|
||||||
App {
|
App {
|
||||||
data: Data {
|
data: Data {
|
||||||
|
lidarr_data: LidarrData::test_default_fully_populated(),
|
||||||
radarr_data: RadarrData::test_default_fully_populated(),
|
radarr_data: RadarrData::test_default_fully_populated(),
|
||||||
sonarr_data: SonarrData::test_default_fully_populated(),
|
sonarr_data: SonarrData::test_default_fully_populated(),
|
||||||
},
|
},
|
||||||
@@ -278,6 +318,12 @@ impl App<'_> {
|
|||||||
contextual_help: None,
|
contextual_help: None,
|
||||||
config: Some(ServarrConfig::default()),
|
config: Some(ServarrConfig::default()),
|
||||||
},
|
},
|
||||||
|
TabRoute {
|
||||||
|
title: "Lidarr".to_owned(),
|
||||||
|
route: ActiveLidarrBlock::Artists.into(),
|
||||||
|
contextual_help: None,
|
||||||
|
config: Some(ServarrConfig::default()),
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
..App::default()
|
..App::default()
|
||||||
}
|
}
|
||||||
@@ -286,6 +332,7 @@ impl App<'_> {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Data<'a> {
|
pub struct Data<'a> {
|
||||||
|
pub lidarr_data: LidarrData<'a>,
|
||||||
pub radarr_data: RadarrData<'a>,
|
pub radarr_data: RadarrData<'a>,
|
||||||
pub sonarr_data: SonarrData<'a>,
|
pub sonarr_data: SonarrData<'a>,
|
||||||
}
|
}
|
||||||
@@ -293,13 +340,14 @@ pub struct Data<'a> {
|
|||||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub theme: Option<String>,
|
pub theme: Option<String>,
|
||||||
|
pub lidarr: Option<Vec<ServarrConfig>>,
|
||||||
pub radarr: Option<Vec<ServarrConfig>>,
|
pub radarr: Option<Vec<ServarrConfig>>,
|
||||||
pub sonarr: Option<Vec<ServarrConfig>>,
|
pub sonarr: Option<Vec<ServarrConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
pub fn validate(&self) {
|
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(
|
log_and_print_error(
|
||||||
"No Servarr configuration provided in the specified configuration file".to_owned(),
|
"No Servarr configuration provided in the specified configuration file".to_owned(),
|
||||||
);
|
);
|
||||||
@@ -313,6 +361,10 @@ impl AppConfig {
|
|||||||
if let Some(sonarr_configs) = &self.sonarr {
|
if let Some(sonarr_configs) = &self.sonarr {
|
||||||
sonarr_configs.iter().for_each(|config| config.validate());
|
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) {
|
pub fn verify_config_present_for_cli(&self, command: &Command) {
|
||||||
@@ -330,6 +382,10 @@ impl AppConfig {
|
|||||||
msg("Sonarr");
|
msg("Sonarr");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
Command::Lidarr(_) if self.lidarr.is_none() => {
|
||||||
|
msg("Lidarr");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,6 +402,12 @@ impl AppConfig {
|
|||||||
sonarr_config.post_process_initialization();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -455,7 +455,6 @@ mod tests {
|
|||||||
let mut app = App::test_default();
|
let mut app = App::test_default();
|
||||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||||
|
|
||||||
// This should panic because the route is not a Sonarr route
|
|
||||||
SonarrContextClueProvider::get_context_clues(&mut app);
|
SonarrContextClueProvider::get_context_clues(&mut app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ mod tests {
|
|||||||
assert_ok!(&result);
|
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]
|
#[test]
|
||||||
fn test_completions_requires_argument() {
|
fn test_completions_requires_argument() {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||||
@@ -174,4 +181,6 @@ mod tests {
|
|||||||
|
|
||||||
assert_ok!(&result);
|
assert_ok!(&result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Implement test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Subcommand, arg};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use super::LidarrCommand;
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
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 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::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,101 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Cli,
|
||||||
|
cli::{
|
||||||
|
Command,
|
||||||
|
lidarr::{LidarrCommand, add_command_handler::LidarrAddCommand},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::LidarrSerdeable;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
models::lidarr_models::DeleteArtistParams,
|
||||||
|
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 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::Artist {
|
||||||
|
artist_id,
|
||||||
|
delete_files_from_disk,
|
||||||
|
add_list_exclusion,
|
||||||
|
} => {
|
||||||
|
let delete_artist_params = DeleteArtistParams {
|
||||||
|
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,196 @@
|
|||||||
|
#[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_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::{DeleteArtistParams, LidarrSerdeable},
|
||||||
|
},
|
||||||
|
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_delete_artist_command() {
|
||||||
|
let expected_delete_artist_params = DeleteArtistParams {
|
||||||
|
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,97 @@
|
|||||||
|
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 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::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,189 @@
|
|||||||
|
#[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_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_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,310 @@
|
|||||||
|
#[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_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::{
|
||||||
|
app::App,
|
||||||
|
cli::{
|
||||||
|
CliCommandHandler,
|
||||||
|
lidarr::{
|
||||||
|
LidarrCliHandler, LidarrCommand, delete_command_handler::LidarrDeleteCommand,
|
||||||
|
list_command_handler::LidarrListCommand,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
models::{
|
||||||
|
Serdeable,
|
||||||
|
lidarr_models::{Artist, DeleteArtistParams, 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 = DeleteArtistParams {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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 = "list_command_handler_tests.rs"]
|
||||||
|
mod list_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum LidarrListCommand {
|
||||||
|
#[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::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,81 @@
|
|||||||
|
#[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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
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 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;
|
||||||
|
|
||||||
|
#[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(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 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::SearchNewArtist { query } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(LidarrEvent::SearchNewArtist(query).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,64 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#[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::*;
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@ use std::sync::Arc;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Subcommand, command};
|
use clap::{Subcommand, command};
|
||||||
use clap_complete::Shell;
|
use clap_complete::Shell;
|
||||||
|
use lidarr::{LidarrCliHandler, LidarrCommand};
|
||||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||||
use sonarr::{SonarrCliHandler, SonarrCommand};
|
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{app::App, network::NetworkTrait};
|
use crate::{app::App, network::NetworkTrait};
|
||||||
|
|
||||||
|
pub mod lidarr;
|
||||||
pub mod radarr;
|
pub mod radarr;
|
||||||
pub mod sonarr;
|
pub mod sonarr;
|
||||||
|
|
||||||
@@ -24,6 +26,9 @@ pub enum Command {
|
|||||||
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
|
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
|
||||||
Sonarr(SonarrCommand),
|
Sonarr(SonarrCommand),
|
||||||
|
|
||||||
|
#[command(subcommand, about = "Commands for manging your Lidarr instance")]
|
||||||
|
Lidarr(LidarrCommand),
|
||||||
|
|
||||||
#[command(
|
#[command(
|
||||||
arg_required_else_help = true,
|
arg_required_else_help = true,
|
||||||
about = "Generate shell completions for the Managarr CLI"
|
about = "Generate shell completions for the Managarr CLI"
|
||||||
@@ -61,6 +66,11 @@ pub(crate) async fn handle_command(
|
|||||||
.handle()
|
.handle()
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
Command::Lidarr(lidarr_command) => {
|
||||||
|
LidarrCliHandler::with(app, lidarr_command, network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use anyhow::Result;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
@@ -49,7 +50,10 @@ impl Events {
|
|||||||
Events { rx }
|
Events { rx }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&self) -> Result<InputEvent<Key>, mpsc::RecvError> {
|
pub fn next(&self) -> Result<Option<InputEvent<Key>>> {
|
||||||
self.rx.recv()
|
match self.rx.try_recv() {
|
||||||
|
Ok(event) => Ok(Some(event)),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ mod property_tests {
|
|||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
|
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::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use crate::models::radarr_models::Movie;
|
use crate::models::{Paginated, Scrollable};
|
||||||
use crate::models::{Scrollable, Paginated};
|
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
/// Property test: Table never panics on index selection
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_index_selection_safety(
|
fn test_table_index_selection_safety(
|
||||||
list_size in list_size(),
|
list_size in list_size(),
|
||||||
@@ -25,19 +24,15 @@ mod property_tests {
|
|||||||
|
|
||||||
table.set_items(movies);
|
table.set_items(movies);
|
||||||
|
|
||||||
// Try to select an arbitrary index
|
|
||||||
if index < list_size {
|
if index < list_size {
|
||||||
table.select_index(Some(index));
|
table.select_index(Some(index));
|
||||||
let selected = table.current_selection();
|
let selected = table.current_selection();
|
||||||
prop_assert_eq!(selected.id, index as i64);
|
prop_assert_eq!(selected.id, index as i64);
|
||||||
} else {
|
} else {
|
||||||
// Out of bounds selection should be safe
|
|
||||||
table.select_index(Some(index));
|
table.select_index(Some(index));
|
||||||
// Should not panic, selection stays valid
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property test: Table state remains consistent after scroll operations
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_scroll_consistency(
|
fn test_table_scroll_consistency(
|
||||||
list_size in list_size(),
|
list_size in list_size(),
|
||||||
@@ -53,42 +48,34 @@ mod property_tests {
|
|||||||
table.set_items(movies);
|
table.set_items(movies);
|
||||||
let initial_id = table.current_selection().id;
|
let initial_id = table.current_selection().id;
|
||||||
|
|
||||||
// Scroll down multiple times
|
|
||||||
for _ in 0..scroll_amount {
|
for _ in 0..scroll_amount {
|
||||||
table.scroll_down();
|
table.scroll_down();
|
||||||
}
|
}
|
||||||
let after_down_id = table.current_selection().id;
|
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 >= initial_id);
|
||||||
prop_assert!(after_down_id < list_size as i64);
|
prop_assert!(after_down_id < list_size as i64);
|
||||||
|
|
||||||
// Scroll back up
|
|
||||||
for _ in 0..scroll_amount {
|
for _ in 0..scroll_amount {
|
||||||
table.scroll_up();
|
table.scroll_up();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should return to initial position (or 0 if we hit the top)
|
|
||||||
prop_assert!(table.current_selection().id <= initial_id);
|
prop_assert!(table.current_selection().id <= initial_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property test: Empty tables handle operations gracefully
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_table_safety(_scroll_ops in 0usize..50) {
|
fn test_empty_table_safety(_scroll_ops in 0usize..50) {
|
||||||
let table = StatefulTable::<Movie>::default();
|
let table = StatefulTable::<Movie>::default();
|
||||||
|
|
||||||
// Empty table operations should be safe
|
|
||||||
prop_assert!(table.is_empty());
|
prop_assert!(table.is_empty());
|
||||||
prop_assert!(table.items.is_empty());
|
prop_assert!(table.items.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property test: Navigation operations maintain consistency
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_navigation_consistency(pushes in 1usize..20) {
|
fn test_navigation_consistency(pushes in 1usize..20) {
|
||||||
let mut app = App::test_default();
|
let mut app = App::test_default();
|
||||||
let initial_route = app.get_current_route();
|
let initial_route = app.get_current_route();
|
||||||
|
|
||||||
// Push multiple routes
|
|
||||||
let routes = vec![
|
let routes = vec![
|
||||||
ActiveRadarrBlock::Movies,
|
ActiveRadarrBlock::Movies,
|
||||||
ActiveRadarrBlock::Collections,
|
ActiveRadarrBlock::Collections,
|
||||||
@@ -101,34 +88,27 @@ mod property_tests {
|
|||||||
app.push_navigation_stack(route.into());
|
app.push_navigation_stack(route.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current route should be the last pushed
|
|
||||||
let last_pushed = routes[(pushes - 1) % routes.len()];
|
let last_pushed = routes[(pushes - 1) % routes.len()];
|
||||||
prop_assert_eq!(app.get_current_route(), last_pushed.into());
|
prop_assert_eq!(app.get_current_route(), last_pushed.into());
|
||||||
|
|
||||||
// Pop all routes
|
|
||||||
for _ in 0..pushes {
|
for _ in 0..pushes {
|
||||||
app.pop_navigation_stack();
|
app.pop_navigation_stack();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should return to initial route
|
|
||||||
prop_assert_eq!(app.get_current_route(), initial_route);
|
prop_assert_eq!(app.get_current_route(), initial_route);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property test: String input handling is safe
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_string_input_safety(input in text_input_string()) {
|
fn test_string_input_safety(input in text_input_string()) {
|
||||||
// String operations should never panic
|
|
||||||
let _lowercase = input.to_lowercase();
|
let _lowercase = input.to_lowercase();
|
||||||
let _uppercase = input.to_uppercase();
|
let _uppercase = input.to_uppercase();
|
||||||
let _trimmed = input.trim();
|
let _trimmed = input.trim();
|
||||||
let _len = input.len();
|
let _len = input.len();
|
||||||
let _chars: Vec<char> = input.chars().collect();
|
let _chars: Vec<char> = input.chars().collect();
|
||||||
|
|
||||||
// All operations completed without panic
|
|
||||||
prop_assert!(true);
|
prop_assert!(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property test: Table maintains data integrity after operations
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_data_integrity(
|
fn test_table_data_integrity(
|
||||||
list_size in 1usize..100
|
list_size in 1usize..100
|
||||||
@@ -144,16 +124,13 @@ mod property_tests {
|
|||||||
table.set_items(movies.clone());
|
table.set_items(movies.clone());
|
||||||
let original_count = table.items.len();
|
let original_count = table.items.len();
|
||||||
|
|
||||||
// Count should remain the same after various operations
|
|
||||||
prop_assert_eq!(table.items.len(), original_count);
|
prop_assert_eq!(table.items.len(), original_count);
|
||||||
|
|
||||||
// All original items should still be present
|
|
||||||
for movie in &movies {
|
for movie in &movies {
|
||||||
prop_assert!(table.items.iter().any(|m| m.id == movie.id));
|
prop_assert!(table.items.iter().any(|m| m.id == movie.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property test: Page up/down maintains bounds
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_page_navigation_bounds(
|
fn test_page_navigation_bounds(
|
||||||
list_size in list_size(),
|
list_size in list_size(),
|
||||||
@@ -168,7 +145,6 @@ mod property_tests {
|
|||||||
|
|
||||||
table.set_items(movies);
|
table.set_items(movies);
|
||||||
|
|
||||||
// Perform page operations
|
|
||||||
for i in 0..page_ops {
|
for i in 0..page_ops {
|
||||||
if i % 2 == 0 {
|
if i % 2 == 0 {
|
||||||
table.page_down();
|
table.page_down();
|
||||||
@@ -176,14 +152,12 @@ mod property_tests {
|
|||||||
table.page_up();
|
table.page_up();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should never exceed bounds
|
|
||||||
let current = table.current_selection();
|
let current = table.current_selection();
|
||||||
prop_assert!(current.id >= 0);
|
prop_assert!(current.id >= 0);
|
||||||
prop_assert!(current.id < list_size as i64);
|
prop_assert!(current.id < list_size as i64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property test: Table filtering reduces or maintains size
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_filter_size_invariant(
|
fn test_table_filter_size_invariant(
|
||||||
list_size in list_size(),
|
list_size in list_size(),
|
||||||
@@ -200,7 +174,6 @@ mod property_tests {
|
|||||||
table.set_items(movies.clone());
|
table.set_items(movies.clone());
|
||||||
let original_size = table.items.len();
|
let original_size = table.items.len();
|
||||||
|
|
||||||
// Apply filter
|
|
||||||
if !filter_term.is_empty() {
|
if !filter_term.is_empty() {
|
||||||
let filtered: Vec<Movie> = movies.into_iter()
|
let filtered: Vec<Movie> = movies.into_iter()
|
||||||
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
|
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
|
||||||
@@ -208,10 +181,8 @@ mod property_tests {
|
|||||||
table.set_items(filtered);
|
table.set_items(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtered size should be <= original
|
|
||||||
prop_assert!(table.items.len() <= original_size);
|
prop_assert!(table.items.len() <= original_size);
|
||||||
|
|
||||||
// Selection should still be valid if table not empty
|
|
||||||
if !table.items.is_empty() {
|
if !table.items.is_empty() {
|
||||||
let current = table.current_selection();
|
let current = table.current_selection();
|
||||||
prop_assert!(current.id >= 0);
|
prop_assert!(current.id >= 0);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ mod tests {
|
|||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
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::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||||
use crate::models::servarr_models::KeybindingItem;
|
use crate::models::servarr_models::KeybindingItem;
|
||||||
@@ -60,11 +61,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)]
|
#[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)]
|
||||||
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)]
|
#[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)]
|
||||||
fn test_handle_change_tabs<T>(#[case] index: usize, #[case] left_block: T, #[case] right_block: T)
|
#[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)]
|
||||||
where
|
fn test_handle_change_tabs<T, U>(
|
||||||
|
#[case] index: usize,
|
||||||
|
#[case] left_block: T,
|
||||||
|
#[case] right_block: U,
|
||||||
|
) where
|
||||||
T: Into<Route> + Copy,
|
T: Into<Route> + Copy,
|
||||||
|
U: Into<Route> + Copy,
|
||||||
{
|
{
|
||||||
let mut app = App::test_default();
|
let mut app = App::test_default();
|
||||||
app.error = "Test".into();
|
app.error = "Test".into();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -75,7 +76,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandle
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
use crate::handlers::KeyEventHandler;
|
||||||
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
|
use crate::models::Route;
|
||||||
|
use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock};
|
||||||
|
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys};
|
||||||
|
|
||||||
|
#[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<'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) {}
|
||||||
|
|
||||||
|
fn handle_scroll_down(&mut self) {}
|
||||||
|
|
||||||
|
fn handle_home(&mut self) {
|
||||||
|
if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput {
|
||||||
|
self
|
||||||
|
.app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.scroll_home();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_end(&mut self) {
|
||||||
|
if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput {
|
||||||
|
self
|
||||||
|
.app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.reset_offset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_delete(&mut self) {}
|
||||||
|
|
||||||
|
fn handle_left_right_action(&mut self) {
|
||||||
|
if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput {
|
||||||
|
handle_text_box_left_right_keys!(
|
||||||
|
self,
|
||||||
|
self.key,
|
||||||
|
self
|
||||||
|
.app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_submit(&mut self) {
|
||||||
|
match self.active_lidarr_block {
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput => {
|
||||||
|
let search_text = &self
|
||||||
|
.app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.text;
|
||||||
|
|
||||||
|
if !search_text.is_empty() {
|
||||||
|
self
|
||||||
|
.app
|
||||||
|
.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
|
||||||
|
self.app.ignore_special_keys_for_textbox_input = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveLidarrBlock::AddArtistSearchResults => {}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_char_key_event(&mut self) {
|
||||||
|
if self.active_lidarr_block == ActiveLidarrBlock::AddArtistSearchInput {
|
||||||
|
handle_text_box_keys!(
|
||||||
|
self,
|
||||||
|
self.key,
|
||||||
|
self
|
||||||
|
.app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_mut(&mut self) -> &mut App<'b> {
|
||||||
|
self.app
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_route(&self) -> Route {
|
||||||
|
self.app.get_current_route()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use pretty_assertions::assert_str_eq;
|
||||||
|
use rstest::rstest;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
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::add_artist_handler::AddArtistHandler;
|
||||||
|
use crate::models::HorizontallyScrollableText;
|
||||||
|
use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock};
|
||||||
|
use crate::models::stateful_table::StatefulTable;
|
||||||
|
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::add_artist_search_result;
|
||||||
|
|
||||||
|
mod test_handle_home_end {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_search_input_home_end_keys() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into());
|
||||||
|
app.data.lidarr_data.add_artist_search = Some("Test".into());
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
DEFAULT_KEYBINDINGS.home.key,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.offset
|
||||||
|
.load(Ordering::SeqCst),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
DEFAULT_KEYBINDINGS.end.key,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.offset
|
||||||
|
.load(Ordering::SeqCst),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod test_handle_left_right_action {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_search_input_left_right_keys() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into());
|
||||||
|
app.data.lidarr_data.add_artist_search = Some("Test".into());
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
DEFAULT_KEYBINDINGS.left.key,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.offset
|
||||||
|
.load(Ordering::SeqCst),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
DEFAULT_KEYBINDINGS.right.key,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.offset
|
||||||
|
.load(Ordering::SeqCst),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod test_handle_submit {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_search_input_submit() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into());
|
||||||
|
app.ignore_special_keys_for_textbox_input = true;
|
||||||
|
app.data.lidarr_data.add_artist_search = Some("test".into());
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
SUBMIT_KEY,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
||||||
|
assert_eq!(
|
||||||
|
app.get_current_route(),
|
||||||
|
ActiveLidarrBlock::AddArtistSearchResults.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_search_input_submit_noop_on_empty_search() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.data.lidarr_data.add_artist_search = Some(HorizontallyScrollableText::default());
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into());
|
||||||
|
app.ignore_special_keys_for_textbox_input = true;
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
SUBMIT_KEY,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert!(app.ignore_special_keys_for_textbox_input);
|
||||||
|
assert_eq!(
|
||||||
|
app.get_current_route(),
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod test_handle_esc {
|
||||||
|
use super::*;
|
||||||
|
use crate::assert_modal_absent;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_add_artist_search_input_esc(#[values(true, false)] is_ready: bool) {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.is_loading = is_ready;
|
||||||
|
app.data.lidarr_data.add_artist_search = Some("test".into());
|
||||||
|
app.ignore_special_keys_for_textbox_input = true;
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into());
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
ESC_KEY,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
||||||
|
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
|
||||||
|
assert_modal_absent!(app.data.lidarr_data.add_artist_search);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_add_artist_search_results_esc(
|
||||||
|
#[values(
|
||||||
|
ActiveLidarrBlock::AddArtistSearchResults,
|
||||||
|
ActiveLidarrBlock::AddArtistEmptySearchResults
|
||||||
|
)]
|
||||||
|
active_lidarr_block: ActiveLidarrBlock,
|
||||||
|
) {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into());
|
||||||
|
app.push_navigation_stack(active_lidarr_block.into());
|
||||||
|
let mut add_searched_artists = StatefulTable::default();
|
||||||
|
add_searched_artists.set_items(vec![add_artist_search_result()]);
|
||||||
|
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
|
||||||
|
|
||||||
|
AddArtistHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle();
|
||||||
|
|
||||||
|
assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchInput.into());
|
||||||
|
assert_modal_absent!(app.data.lidarr_data.add_searched_artists);
|
||||||
|
assert!(app.ignore_special_keys_for_textbox_input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod test_handle_key_char {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_search_input_backspace() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||||
|
app.data.lidarr_data.add_artist_search = Some("Test".into());
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
DEFAULT_KEYBINDINGS.backspace.key,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.text,
|
||||||
|
"Tes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_search_input_char_key() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||||
|
app.data.lidarr_data.add_artist_search = Some(HorizontallyScrollableText::default());
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
Key::Char('a'),
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.add_artist_search
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.text,
|
||||||
|
"a"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_handler_accepts() {
|
||||||
|
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||||
|
if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) {
|
||||||
|
assert!(AddArtistHandler::accepts(active_lidarr_block));
|
||||||
|
} else {
|
||||||
|
assert!(!AddArtistHandler::accepts(active_lidarr_block));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_add_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 = AddArtistHandler::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_add_artist_search_no_panic_on_none_search_result() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.data.lidarr_data.add_searched_artists = None;
|
||||||
|
|
||||||
|
AddArtistHandler::new(
|
||||||
|
DEFAULT_KEYBINDINGS.esc.key,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchResults,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_handler_is_not_ready_when_loading() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.is_loading = true;
|
||||||
|
|
||||||
|
let handler = AddArtistHandler::new(
|
||||||
|
DEFAULT_KEYBINDINGS.esc.key,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!handler.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_handler_is_ready_when_not_loading() {
|
||||||
|
let mut app = App::test_default();
|
||||||
|
app.is_loading = false;
|
||||||
|
|
||||||
|
let handler = AddArtistHandler::new(
|
||||||
|
DEFAULT_KEYBINDINGS.esc.key,
|
||||||
|
&mut app,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(handler.is_ready());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
use crate::models::Route;
|
||||||
|
use crate::models::lidarr_models::DeleteArtistParams;
|
||||||
|
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) -> DeleteArtistParams {
|
||||||
|
let id = self.app.data.lidarr_data.artists.current_selection().id;
|
||||||
|
let delete_files = self.app.data.lidarr_data.delete_artist_files;
|
||||||
|
let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion;
|
||||||
|
self.app.data.lidarr_data.reset_delete_artist_preferences();
|
||||||
|
|
||||||
|
DeleteArtistParams {
|
||||||
|
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_artist_preferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.app.pop_navigation_stack();
|
||||||
|
}
|
||||||
|
ActiveLidarrBlock::DeleteArtistToggleDeleteFile => {
|
||||||
|
self.app.data.lidarr_data.delete_artist_files =
|
||||||
|
!self.app.data.lidarr_data.delete_artist_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_artist_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, DeleteArtistParams};
|
||||||
|
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_artist_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_artist_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_artist_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 = DeleteArtistParams {
|
||||||
|
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_artist_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_artist_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_artist_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_artist_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_artist_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_artist_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_artist_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_artist_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 = DeleteArtistParams {
|
||||||
|
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_artist_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_artist_files = true;
|
||||||
|
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||||
|
let expected_delete_artist_params = DeleteArtistParams {
|
||||||
|
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_artist_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,664 @@
|
|||||||
|
#[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::{Artist, ArtistStatistics, ArtistStatus};
|
||||||
|
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||||
|
ADD_ARTIST_BLOCKS, ActiveLidarrBlock, 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(DELETE_ARTIST_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_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());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,317 @@
|
|||||||
|
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 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 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();
|
||||||
|
}
|
||||||
|
_ => self.handle_key_event(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||||
|
AddArtistHandler::accepts(active_block)
|
||||||
|
|| DeleteArtistHandler::accepts(active_block)
|
||||||
|
|| EditArtistHandler::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) {
|
||||||
|
if self.active_lidarr_block == 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 radarr_handlers::RadarrHandler;
|
||||||
use sonarr_handlers::SonarrHandler;
|
use sonarr_handlers::SonarrHandler;
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ use crate::models::stateful_table::StatefulTable;
|
|||||||
use crate::models::{HorizontallyScrollableText, Route};
|
use crate::models::{HorizontallyScrollableText, Route};
|
||||||
|
|
||||||
mod keybinding_handler;
|
mod keybinding_handler;
|
||||||
|
mod lidarr_handlers;
|
||||||
mod radarr_handlers;
|
mod radarr_handlers;
|
||||||
mod sonarr_handlers;
|
mod sonarr_handlers;
|
||||||
|
|
||||||
@@ -125,6 +127,9 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
|||||||
Route::Sonarr(active_sonarr_block, context) => {
|
Route::Sonarr(active_sonarr_block, context) => {
|
||||||
SonarrHandler::new(key, app, active_sonarr_block, context).handle()
|
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(_, _) => {
|
Route::Sonarr(_, _) => {
|
||||||
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
|
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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::radarr_models::BlocklistItem;
|
use crate::models::radarr_models::BlocklistItem;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use crate::event::Key;
|
|||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::BlockSelectionState;
|
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
||||||
EDIT_COLLECTION_SELECTION_BLOCKS,
|
EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
|
use crate::models::{BlockSelectionState, Route};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "collection_details_handler_tests.rs"]
|
#[path = "collection_details_handler_tests.rs"]
|
||||||
@@ -148,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
use crate::models::Scrollable;
|
|
||||||
use crate::models::radarr_models::EditCollectionParams;
|
use crate::models::radarr_models::EditCollectionParams;
|
||||||
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
|
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
|
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::network::radarr_network::RadarrEvent;
|
||||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
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
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::BlockSelectionState;
|
|
||||||
use crate::models::radarr_models::Collection;
|
use crate::models::radarr_models::Collection;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
|
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
|
use crate::models::{BlockSelectionState, Route};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
|
||||||
mod collection_details_handler;
|
mod collection_details_handler;
|
||||||
@@ -179,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
|
||||||
use crate::models::servarr_models::EditIndexerParams;
|
use crate::models::servarr_models::EditIndexerParams;
|
||||||
@@ -527,7 +528,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::radarr_models::IndexerSettings;
|
use crate::models::radarr_models::IndexerSettings;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
|
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||||
@@ -293,7 +294,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::BlockSelectionState;
|
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||||
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::{BlockSelectionState, Route};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
|
||||||
mod edit_indexer_handler;
|
mod edit_indexer_handler;
|
||||||
@@ -212,7 +212,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock,
|
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::network::radarr_network::RadarrEvent;
|
||||||
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||||
|
|
||||||
@@ -558,7 +558,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::radarr_models::DeleteMovieParams;
|
use crate::models::radarr_models::DeleteMovieParams;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
@@ -141,7 +142,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
use crate::models::Scrollable;
|
|
||||||
use crate::models::radarr_models::EditMovieParams;
|
use crate::models::radarr_models::EditMovieParams;
|
||||||
use crate::models::servarr_data::radarr::modals::EditMovieModal;
|
use crate::models::servarr_data::radarr::modals::EditMovieModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS};
|
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::network::radarr_network::RadarrEvent;
|
||||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
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
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::servarr_models::Language;
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
use crate::models::{BlockSelectionState, Scrollable};
|
use crate::models::{BlockSelectionState, Route, Scrollable};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -379,7 +379,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::library::LibraryHandler;
|
||||||
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
|
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
|
||||||
use crate::handlers::radarr_handlers::system::SystemHandler;
|
use crate::handlers::radarr_handlers::system::SystemHandler;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::{App, Key, matches_key};
|
use crate::{App, Key, matches_key};
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use crate::event::Key;
|
|||||||
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
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_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS};
|
||||||
use crate::models::servarr_models::AddRootFolderBody;
|
use crate::models::servarr_models::AddRootFolderBody;
|
||||||
|
use crate::models::{HorizontallyScrollableText, Route};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
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
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::Scrollable;
|
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
|
use crate::models::{Route, Scrollable};
|
||||||
|
|
||||||
mod system_details_handler;
|
mod system_details_handler;
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ use crate::app::App;
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::Scrollable;
|
|
||||||
use crate::models::radarr_models::RadarrTaskName;
|
use crate::models::radarr_models::RadarrTaskName;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
|
||||||
use crate::models::stateful_list::StatefulList;
|
use crate::models::stateful_list::StatefulList;
|
||||||
|
use crate::models::{Route, Scrollable};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS};
|
||||||
use crate::models::sonarr_models::BlocklistItem;
|
use crate::models::sonarr_models::BlocklistItem;
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS};
|
||||||
use crate::models::servarr_models::Language;
|
use crate::models::servarr_models::Language;
|
||||||
use crate::models::sonarr_models::SonarrHistoryItem;
|
use crate::models::sonarr_models::SonarrHistoryItem;
|
||||||
@@ -121,7 +122,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
|
||||||
use crate::models::servarr_models::EditIndexerParams;
|
use crate::models::servarr_models::EditIndexerParams;
|
||||||
@@ -526,7 +527,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||||
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS,
|
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||||
};
|
};
|
||||||
@@ -202,7 +203,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::BlockSelectionState;
|
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||||
ActiveSonarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
ActiveSonarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||||
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::{BlockSelectionState, Route};
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
|
|
||||||
mod edit_indexer_handler;
|
mod edit_indexer_handler;
|
||||||
@@ -211,7 +211,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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,
|
ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, ActiveSonarrBlock,
|
||||||
};
|
};
|
||||||
use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult};
|
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::network::sonarr_network::SonarrEvent;
|
||||||
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||||
|
|
||||||
@@ -625,7 +625,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::models::Route;
|
||||||
use crate::models::sonarr_models::DeleteSeriesParams;
|
use crate::models::sonarr_models::DeleteSeriesParams;
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -143,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
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::modals::EditSeriesModal;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS};
|
||||||
use crate::models::sonarr_models::EditSeriesParams;
|
use crate::models::sonarr_models::EditSeriesParams;
|
||||||
|
use crate::models::{Route, Scrollable};
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
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
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS};
|
||||||
use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody};
|
use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody};
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
@@ -370,7 +371,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandle
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::season_details_handler::SeasonDetailsHandler;
|
||||||
use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler;
|
use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler;
|
||||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
|
use crate::models::Route;
|
||||||
|
|
||||||
mod add_series_handler;
|
mod add_series_handler;
|
||||||
mod delete_series_handler;
|
mod delete_series_handler;
|
||||||
@@ -245,7 +246,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS};
|
||||||
use crate::models::servarr_models::Language;
|
use crate::models::servarr_models::Language;
|
||||||
use crate::models::sonarr_models::{
|
use crate::models::sonarr_models::{
|
||||||
@@ -458,7 +459,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::BlockSelectionState;
|
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||||
ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS,
|
ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
|
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
|
||||||
|
use crate::models::{BlockSelectionState, Route};
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -342,7 +342,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ use library::LibraryHandler;
|
|||||||
use root_folders::RootFoldersHandler;
|
use root_folders::RootFoldersHandler;
|
||||||
use system::SystemHandler;
|
use system::SystemHandler;
|
||||||
|
|
||||||
|
use super::KeyEventHandler;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App, event::Key, matches_key, models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
|
app::App, event::Key, matches_key, models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::KeyEventHandler;
|
|
||||||
|
|
||||||
mod blocklist;
|
mod blocklist;
|
||||||
mod downloads;
|
mod downloads;
|
||||||
mod history;
|
mod history;
|
||||||
@@ -115,7 +115,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
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_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS};
|
||||||
use crate::models::servarr_models::AddRootFolderBody;
|
use crate::models::servarr_models::AddRootFolderBody;
|
||||||
|
use crate::models::{HorizontallyScrollableText, Route};
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
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
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_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::sonarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::Scrollable;
|
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||||
|
use crate::models::{Route, Scrollable};
|
||||||
|
|
||||||
mod system_details_handler;
|
mod system_details_handler;
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemHandler<'a, 'b
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ use crate::app::App;
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||||
use crate::matches_key;
|
use crate::matches_key;
|
||||||
use crate::models::Scrollable;
|
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS};
|
||||||
use crate::models::sonarr_models::SonarrTaskName;
|
use crate::models::sonarr_models::SonarrTaskName;
|
||||||
use crate::models::stateful_list::StatefulList;
|
use crate::models::stateful_list::StatefulList;
|
||||||
|
use crate::models::{Route, Scrollable};
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod tests {
|
|||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::handlers::table_handler::TableHandlingConfig;
|
use crate::handlers::table_handler::TableHandlingConfig;
|
||||||
use crate::handlers::table_handler::handle_table;
|
use crate::handlers::table_handler::handle_table;
|
||||||
|
use crate::models::Route;
|
||||||
use crate::models::radarr_models::Movie;
|
use crate::models::radarr_models::Movie;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::models::servarr_models::Language;
|
use crate::models::servarr_models::Language;
|
||||||
@@ -98,7 +99,7 @@ mod tests {
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> crate::models::Route {
|
fn current_route(&self) -> Route {
|
||||||
self.app.get_current_route()
|
self.app.get_current_route()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-10
@@ -3,12 +3,15 @@
|
|||||||
extern crate assertables;
|
extern crate assertables;
|
||||||
|
|
||||||
use anyhow::Result;
|
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 clap_complete::generate;
|
||||||
use crossterm::execute;
|
use crossterm::execute;
|
||||||
use crossterm::terminal::{
|
use crossterm::terminal::{
|
||||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||||
};
|
};
|
||||||
|
use indoc::indoc;
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use network::NetworkTrait;
|
use network::NetworkTrait;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
@@ -64,6 +67,13 @@ mod utils;
|
|||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Command>,
|
command: Option<Command>,
|
||||||
|
#[command(flatten)]
|
||||||
|
global: GlobalOpts,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
#[command(next_help_heading = "Global Options")]
|
||||||
|
struct GlobalOpts {
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
global = true,
|
global = true,
|
||||||
@@ -98,9 +108,12 @@ struct Cli {
|
|||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
global = true,
|
global = true,
|
||||||
help = "For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
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.
|
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."
|
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
|
||||||
|
"}
|
||||||
)]
|
)]
|
||||||
servarr_name: Option<String>,
|
servarr_name: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -114,13 +127,13 @@ async fn main() -> Result<()> {
|
|||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
let r = running.clone();
|
let r = running.clone();
|
||||||
let args = Cli::parse();
|
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"))?
|
load_config(config_file.to_str().expect("Invalid config file specified"))?
|
||||||
} else {
|
} else {
|
||||||
confy::load("managarr", "config")?
|
confy::load("managarr", "config")?
|
||||||
};
|
};
|
||||||
let theme_name = config.theme.clone();
|
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:?}");
|
debug!("Managarr loaded using config: {config:?}");
|
||||||
config.validate();
|
config.validate();
|
||||||
config.post_process_initialization();
|
config.post_process_initialization();
|
||||||
@@ -145,7 +158,7 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(command) => match command {
|
Some(command) => match command {
|
||||||
Command::Radarr(_) | Command::Sonarr(_) => {
|
Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) => {
|
||||||
if spinner_disabled {
|
if spinner_disabled {
|
||||||
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||||
} else {
|
} else {
|
||||||
@@ -165,8 +178,8 @@ async fn main() -> Result<()> {
|
|||||||
});
|
});
|
||||||
start_ui(
|
start_ui(
|
||||||
&app,
|
&app,
|
||||||
&args.themes_file,
|
&args.global.themes_file,
|
||||||
args.theme.unwrap_or(theme_name.unwrap_or_default()),
|
args.global.theme.unwrap_or(theme_name.unwrap_or_default()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@@ -249,7 +262,7 @@ async fn start_ui(
|
|||||||
terminal.draw(|f| ui(f, &mut app))?;
|
terminal.draw(|f| ui(f, &mut app))?;
|
||||||
|
|
||||||
match input_events.next()? {
|
match input_events.next()? {
|
||||||
InputEvent::KeyEvent(key) => {
|
Some(InputEvent::KeyEvent(key)) => {
|
||||||
if key == Key::Char('q') && !app.ignore_special_keys_for_textbox_input {
|
if key == Key::Char('q') && !app.ignore_special_keys_for_textbox_input {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -257,7 +270,8 @@ async fn start_ui(
|
|||||||
handlers::handle_events(key, &mut app);
|
handlers::handle_events(key, &mut app);
|
||||||
}
|
}
|
||||||
|
|
||||||
InputEvent::Tick => app.on_tick().await,
|
Some(InputEvent::Tick) => app.on_tick().await,
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
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(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(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub struct DeleteArtistParams {
|
||||||
|
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 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LidarrSerdeable> for Serdeable {
|
||||||
|
fn from(value: LidarrSerdeable) -> Serdeable {
|
||||||
|
Serdeable::Lidarr(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_enum_from!(
|
||||||
|
LidarrSerdeable {
|
||||||
|
AddArtistSearchResults(Vec<AddArtistSearchResult>),
|
||||||
|
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,495 @@
|
|||||||
|
#[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, DownloadRecord, DownloadStatus, DownloadsResponse, Member,
|
||||||
|
MetadataProfile, 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_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_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-3
@@ -3,7 +3,9 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
|
|
||||||
use crate::app::ServarrConfig;
|
use crate::app::ServarrConfig;
|
||||||
use crate::app::context_clues::ContextClue;
|
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 crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
|
use lidarr_models::LidarrSerdeable;
|
||||||
use radarr_models::RadarrSerdeable;
|
use radarr_models::RadarrSerdeable;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||||
@@ -11,6 +13,7 @@ use serde_json::Number;
|
|||||||
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||||
use sonarr_models::SonarrSerdeable;
|
use sonarr_models::SonarrSerdeable;
|
||||||
|
|
||||||
|
pub mod lidarr_models;
|
||||||
pub mod radarr_models;
|
pub mod radarr_models;
|
||||||
pub mod servarr_data;
|
pub mod servarr_data;
|
||||||
pub mod servarr_models;
|
pub mod servarr_models;
|
||||||
@@ -30,7 +33,7 @@ pub enum Route {
|
|||||||
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
|
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
|
||||||
Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
|
Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
|
||||||
Readarr,
|
Readarr,
|
||||||
Lidarr,
|
Lidarr(ActiveLidarrBlock, Option<ActiveLidarrBlock>),
|
||||||
Whisparr,
|
Whisparr,
|
||||||
Bazarr,
|
Bazarr,
|
||||||
Prowlarr,
|
Prowlarr,
|
||||||
@@ -43,6 +46,7 @@ pub enum Route {
|
|||||||
pub enum Serdeable {
|
pub enum Serdeable {
|
||||||
Radarr(RadarrSerdeable),
|
Radarr(RadarrSerdeable),
|
||||||
Sonarr(SonarrSerdeable),
|
Sonarr(SonarrSerdeable),
|
||||||
|
Lidarr(LidarrSerdeable),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Scrollable {
|
pub trait Scrollable {
|
||||||
@@ -289,8 +293,7 @@ impl TabState {
|
|||||||
TabState { tabs, index: 0 }
|
TabState { tabs, index: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allowing this code for now since we'll eventually be implementing additional Servarr support, and we'll need it then
|
#[cfg(test)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_index(&mut self, index: usize) -> &TabRoute {
|
pub fn set_index(&mut self, index: usize) -> &TabRoute {
|
||||||
self.index = index;
|
self.index = index;
|
||||||
&self.tabs[self.index]
|
&self.tabs[self.index]
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
use serde_json::Number;
|
||||||
|
|
||||||
|
use super::modals::EditArtistModal;
|
||||||
|
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
|
||||||
|
use crate::models::{
|
||||||
|
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
|
||||||
|
lidarr_models::{AddArtistSearchResult, Artist, DownloadRecord},
|
||||||
|
servarr_models::{DiskSpace, RootFolder},
|
||||||
|
stateful_table::StatefulTable,
|
||||||
|
};
|
||||||
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
|
use bimap::BiMap;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use strum::EnumIter;
|
||||||
|
#[cfg(test)]
|
||||||
|
use {
|
||||||
|
crate::models::lidarr_models::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, 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_search: Option<HorizontallyScrollableText>,
|
||||||
|
pub add_import_list_exclusion: bool,
|
||||||
|
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
|
||||||
|
pub artists: StatefulTable<Artist>,
|
||||||
|
pub delete_artist_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_artist_preferences(&mut self) {
|
||||||
|
self.delete_artist_files = false;
|
||||||
|
self.add_import_list_exclusion = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
let mut quality_profile_names: Vec<String> =
|
||||||
|
self.quality_profile_map.right_values().cloned().collect();
|
||||||
|
quality_profile_names.sort();
|
||||||
|
quality_profile_names
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sorted_metadata_profile_names(&self) -> Vec<String> {
|
||||||
|
let mut metadata_profile_names: Vec<String> =
|
||||||
|
self.metadata_profile_map.right_values().cloned().collect();
|
||||||
|
metadata_profile_names.sort();
|
||||||
|
metadata_profile_names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for LidarrData<'a> {
|
||||||
|
fn default() -> LidarrData<'a> {
|
||||||
|
LidarrData {
|
||||||
|
add_artist_search: None,
|
||||||
|
add_import_list_exclusion: false,
|
||||||
|
add_searched_artists: None,
|
||||||
|
artists: StatefulTable::default(),
|
||||||
|
delete_artist_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,
|
||||||
|
}]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl LidarrData<'_> {
|
||||||
|
pub fn test_default_fully_populated() -> Self {
|
||||||
|
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_artist_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),
|
||||||
|
tags_map: tags_map(),
|
||||||
|
..LidarrData::default()
|
||||||
|
};
|
||||||
|
lidarr_data.artists.set_items(vec![Artist::default()]);
|
||||||
|
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,
|
||||||
|
ArtistsSortPrompt,
|
||||||
|
AddArtistEmptySearchResults,
|
||||||
|
AddArtistSearchInput,
|
||||||
|
AddArtistSearchResults,
|
||||||
|
DeleteArtistPrompt,
|
||||||
|
DeleteArtistConfirmPrompt,
|
||||||
|
DeleteArtistToggleDeleteFile,
|
||||||
|
DeleteArtistToggleAddListExclusion,
|
||||||
|
EditArtistPrompt,
|
||||||
|
EditArtistConfirmPrompt,
|
||||||
|
EditArtistPathInput,
|
||||||
|
EditArtistSelectMetadataProfile,
|
||||||
|
EditArtistSelectMonitorNewItems,
|
||||||
|
EditArtistSelectQualityProfile,
|
||||||
|
EditArtistTagsInput,
|
||||||
|
EditArtistToggleMonitored,
|
||||||
|
FilterArtists,
|
||||||
|
FilterArtistsError,
|
||||||
|
SearchArtists,
|
||||||
|
SearchArtistsError,
|
||||||
|
UpdateAllArtistsPrompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
|
||||||
|
ActiveLidarrBlock::Artists,
|
||||||
|
ActiveLidarrBlock::ArtistsSortPrompt,
|
||||||
|
ActiveLidarrBlock::FilterArtists,
|
||||||
|
ActiveLidarrBlock::FilterArtistsError,
|
||||||
|
ActiveLidarrBlock::SearchArtists,
|
||||||
|
ActiveLidarrBlock::SearchArtistsError,
|
||||||
|
ActiveLidarrBlock::UpdateAllArtistsPrompt,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 3] = [
|
||||||
|
ActiveLidarrBlock::AddArtistEmptySearchResults,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchInput,
|
||||||
|
ActiveLidarrBlock::AddArtistSearchResults,
|
||||||
|
];
|
||||||
|
|
||||||
|
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 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,239 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
|
||||||
|
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||||
|
ADD_ARTIST_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_artist_preferences() {
|
||||||
|
let mut lidarr_data = LidarrData {
|
||||||
|
delete_artist_files: true,
|
||||||
|
add_import_list_exclusion: true,
|
||||||
|
..LidarrData::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
lidarr_data.reset_delete_artist_preferences();
|
||||||
|
|
||||||
|
assert!(!lidarr_data.delete_artist_files);
|
||||||
|
assert!(!lidarr_data.add_import_list_exclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 3".to_owned());
|
||||||
|
quality_profile_map.insert(2, "test 2".to_owned());
|
||||||
|
quality_profile_map.insert(1, "test 1".to_owned());
|
||||||
|
let lidarr_data = LidarrData {
|
||||||
|
quality_profile_map,
|
||||||
|
..LidarrData::default()
|
||||||
|
};
|
||||||
|
let expected_quality_profile_vec = vec![
|
||||||
|
"test 1".to_owned(),
|
||||||
|
"test 2".to_owned(),
|
||||||
|
"test 3".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 3".to_owned());
|
||||||
|
metadata_profile_map.insert(2, "test 2".to_owned());
|
||||||
|
metadata_profile_map.insert(1, "test 1".to_owned());
|
||||||
|
let lidarr_data = LidarrData {
|
||||||
|
metadata_profile_map,
|
||||||
|
..LidarrData::default()
|
||||||
|
};
|
||||||
|
let expected_metadata_profile_vec = vec![
|
||||||
|
"test 1".to_owned(),
|
||||||
|
"test 2".to_owned(),
|
||||||
|
"test 3".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.artists);
|
||||||
|
assert!(!lidarr_data.delete_artist_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_add_artist_blocks_contents() {
|
||||||
|
assert_eq!(ADD_ARTIST_BLOCKS.len(), 3);
|
||||||
|
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistEmptySearchResults));
|
||||||
|
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchInput));
|
||||||
|
assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchResults));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_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,78 @@
|
|||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use super::lidarr_data::LidarrData;
|
||||||
|
use crate::models::{
|
||||||
|
HorizontallyScrollableText, lidarr_models::NewItemMonitorType, stateful_list::StatefulList,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "modals_tests.rs"]
|
||||||
|
mod modals_tests;
|
||||||
|
|
||||||
|
#[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,54 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use bimap::BiMap;
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::models::lidarr_models::{Artist, NewItemMonitorType};
|
||||||
|
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
|
||||||
|
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
|
|
||||||
|
pub mod lidarr;
|
||||||
pub mod modals;
|
pub mod modals;
|
||||||
pub mod radarr;
|
pub mod radarr;
|
||||||
pub mod sonarr;
|
pub mod sonarr;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
mod radarr_data_tests {
|
mod radarr_data_tests {
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
|
||||||
|
|
||||||
use crate::app::context_clues::{
|
use crate::app::context_clues::{
|
||||||
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||||
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||||
@@ -12,6 +9,10 @@ mod tests {
|
|||||||
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
|
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
|
||||||
MOVIE_DETAILS_CONTEXT_CLUES,
|
MOVIE_DETAILS_CONTEXT_CLUES,
|
||||||
};
|
};
|
||||||
|
use bimap::BiMap;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde_json::Number;
|
||||||
|
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils;
|
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils;
|
||||||
@@ -61,6 +62,45 @@ mod tests {
|
|||||||
assert_movie_info_tabs_reset!(radarr_data);
|
assert_movie_info_tabs_reset!(radarr_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 radarr_data = RadarrData {
|
||||||
|
tags_map,
|
||||||
|
..RadarrData::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
radarr_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 3".to_owned());
|
||||||
|
quality_profile_map.insert(2, "test 2".to_owned());
|
||||||
|
quality_profile_map.insert(1, "test 1".to_owned());
|
||||||
|
let radarr_data = RadarrData {
|
||||||
|
quality_profile_map,
|
||||||
|
..RadarrData::default()
|
||||||
|
};
|
||||||
|
let expected_quality_profile_vec = vec![
|
||||||
|
"test 1".to_owned(),
|
||||||
|
"test 2".to_owned(),
|
||||||
|
"test 3".to_owned(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_iter_eq!(
|
||||||
|
radarr_data.sorted_quality_profile_names(),
|
||||||
|
expected_quality_profile_vec
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_data_defaults() {
|
fn test_radarr_data_defaults() {
|
||||||
let radarr_data = RadarrData::default();
|
let radarr_data = RadarrData::default();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use crate::{
|
|||||||
mod modals_tests;
|
mod modals_tests;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
#[cfg_attr(test, derive(Debug))]
|
||||||
pub struct AddSeriesModal {
|
pub struct AddSeriesModal {
|
||||||
pub root_folder_list: StatefulList<RootFolder>,
|
pub root_folder_list: StatefulList<RootFolder>,
|
||||||
pub monitor_list: StatefulList<SeriesMonitor>,
|
pub monitor_list: StatefulList<SeriesMonitor>,
|
||||||
@@ -130,6 +131,7 @@ impl From<&SonarrData<'_>> for EditIndexerModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
#[cfg_attr(test, derive(Debug))]
|
||||||
pub struct EditSeriesModal {
|
pub struct EditSeriesModal {
|
||||||
pub series_type_list: StatefulList<SeriesType>,
|
pub series_type_list: StatefulList<SeriesType>,
|
||||||
pub quality_profile_list: StatefulList<String>,
|
pub quality_profile_list: StatefulList<String>,
|
||||||
@@ -260,6 +262,7 @@ impl Default for EpisodeDetailsModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(Debug))]
|
||||||
pub struct SeasonDetailsModal {
|
pub struct SeasonDetailsModal {
|
||||||
pub episodes: StatefulTable<Episode>,
|
pub episodes: StatefulTable<Episode>,
|
||||||
pub episode_files: StatefulTable<EpisodeFile>,
|
pub episode_files: StatefulTable<EpisodeFile>,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
mod sonarr_data_tests {
|
mod sonarr_data_tests {
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
|
||||||
|
|
||||||
use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES;
|
use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES;
|
||||||
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
|
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
@@ -22,6 +19,10 @@ mod tests {
|
|||||||
servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData},
|
servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use bimap::BiMap;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde_json::Number;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_active_sonarr_block_to_route() {
|
fn test_from_active_sonarr_block_to_route() {
|
||||||
@@ -77,44 +78,105 @@ mod tests {
|
|||||||
assert_eq!(sonarr_data.series_info_tabs.index, 0);
|
assert_eq!(sonarr_data.series_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 sonarr_data = SonarrData {
|
||||||
|
tags_map,
|
||||||
|
..SonarrData::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
sonarr_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 3".to_owned());
|
||||||
|
quality_profile_map.insert(2, "test 2".to_owned());
|
||||||
|
quality_profile_map.insert(1, "test 1".to_owned());
|
||||||
|
let sonarr_data = SonarrData {
|
||||||
|
quality_profile_map,
|
||||||
|
..SonarrData::default()
|
||||||
|
};
|
||||||
|
let expected_quality_profile_vec = vec![
|
||||||
|
"test 1".to_owned(),
|
||||||
|
"test 2".to_owned(),
|
||||||
|
"test 3".to_owned(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_iter_eq!(
|
||||||
|
sonarr_data.sorted_quality_profile_names(),
|
||||||
|
expected_quality_profile_vec
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sorted_language_profile_names() {
|
||||||
|
let mut language_profiles_map = BiMap::new();
|
||||||
|
language_profiles_map.insert(3, "test 3".to_owned());
|
||||||
|
language_profiles_map.insert(2, "test 2".to_owned());
|
||||||
|
language_profiles_map.insert(1, "test 1".to_owned());
|
||||||
|
let sonarr_data = SonarrData {
|
||||||
|
language_profiles_map,
|
||||||
|
..SonarrData::default()
|
||||||
|
};
|
||||||
|
let expected_language_profiles_vec = vec![
|
||||||
|
"test 1".to_owned(),
|
||||||
|
"test 2".to_owned(),
|
||||||
|
"test 3".to_owned(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_iter_eq!(
|
||||||
|
sonarr_data.sorted_language_profile_names(),
|
||||||
|
expected_language_profiles_vec
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sonarr_data_defaults() {
|
fn test_sonarr_data_defaults() {
|
||||||
let sonarr_data = SonarrData::default();
|
let sonarr_data = SonarrData::default();
|
||||||
|
|
||||||
assert!(!sonarr_data.add_list_exclusion);
|
assert!(!sonarr_data.add_list_exclusion);
|
||||||
assert!(sonarr_data.add_searched_series.is_none());
|
assert_none!(sonarr_data.add_searched_series);
|
||||||
assert!(sonarr_data.add_series_search.is_none());
|
assert_none!(sonarr_data.add_series_search);
|
||||||
assert!(sonarr_data.add_series_modal.is_none());
|
assert_none!(sonarr_data.add_series_modal);
|
||||||
assert!(sonarr_data.blocklist.is_empty());
|
assert_is_empty!(sonarr_data.blocklist);
|
||||||
assert!(!sonarr_data.delete_series_files);
|
assert!(!sonarr_data.delete_series_files);
|
||||||
assert!(sonarr_data.downloads.is_empty());
|
assert_is_empty!(sonarr_data.downloads);
|
||||||
assert!(sonarr_data.disk_space_vec.is_empty());
|
assert_is_empty!(sonarr_data.disk_space_vec);
|
||||||
assert!(sonarr_data.edit_indexer_modal.is_none());
|
assert_none!(sonarr_data.edit_indexer_modal);
|
||||||
assert!(sonarr_data.edit_root_folder.is_none());
|
assert_none!(sonarr_data.edit_root_folder);
|
||||||
assert!(sonarr_data.edit_series_modal.is_none());
|
assert_none!(sonarr_data.edit_series_modal);
|
||||||
assert!(sonarr_data.history.is_empty());
|
assert_is_empty!(sonarr_data.history);
|
||||||
assert!(sonarr_data.indexers.is_empty());
|
assert_is_empty!(sonarr_data.indexers);
|
||||||
assert!(sonarr_data.indexer_settings.is_none());
|
assert_none!(sonarr_data.indexer_settings);
|
||||||
assert!(sonarr_data.indexer_test_errors.is_none());
|
assert_none!(sonarr_data.indexer_test_errors);
|
||||||
assert!(sonarr_data.indexer_test_all_results.is_none());
|
assert_none!(sonarr_data.indexer_test_all_results);
|
||||||
assert!(sonarr_data.language_profiles_map.is_empty());
|
assert_is_empty!(sonarr_data.language_profiles_map);
|
||||||
assert!(sonarr_data.logs.is_empty());
|
assert_is_empty!(sonarr_data.logs);
|
||||||
assert!(sonarr_data.log_details.is_empty());
|
assert_is_empty!(sonarr_data.log_details);
|
||||||
assert!(!sonarr_data.prompt_confirm);
|
assert!(!sonarr_data.prompt_confirm);
|
||||||
assert!(sonarr_data.prompt_confirm_action.is_none());
|
assert_none!(sonarr_data.prompt_confirm_action);
|
||||||
assert!(sonarr_data.quality_profile_map.is_empty());
|
assert_is_empty!(sonarr_data.quality_profile_map);
|
||||||
assert!(sonarr_data.queued_events.is_empty());
|
assert_is_empty!(sonarr_data.queued_events);
|
||||||
assert!(sonarr_data.root_folders.is_empty());
|
assert_is_empty!(sonarr_data.root_folders);
|
||||||
assert!(sonarr_data.seasons.is_empty());
|
assert_is_empty!(sonarr_data.seasons);
|
||||||
assert!(sonarr_data.season_details_modal.is_none());
|
assert_none!(sonarr_data.season_details_modal);
|
||||||
assert_eq!(sonarr_data.selected_block, BlockSelectionState::default());
|
assert_eq!(sonarr_data.selected_block, BlockSelectionState::default());
|
||||||
assert!(sonarr_data.series.is_empty());
|
assert_is_empty!(sonarr_data.series);
|
||||||
assert!(sonarr_data.series_history.is_none());
|
assert_none!(sonarr_data.series_history);
|
||||||
assert_eq!(sonarr_data.start_time, <DateTime<Utc>>::default());
|
assert_eq!(sonarr_data.start_time, <DateTime<Utc>>::default());
|
||||||
assert!(sonarr_data.tags_map.is_empty());
|
assert_is_empty!(sonarr_data.tags_map);
|
||||||
assert!(sonarr_data.tasks.is_empty());
|
assert_is_empty!(sonarr_data.tasks);
|
||||||
assert!(sonarr_data.updates.is_empty());
|
assert_is_empty!(sonarr_data.updates);
|
||||||
assert!(sonarr_data.version.is_empty());
|
assert_is_empty!(sonarr_data.version);
|
||||||
|
|
||||||
assert_eq!(sonarr_data.main_tabs.tabs.len(), 7);
|
assert_eq!(sonarr_data.main_tabs.tabs.len(), 7);
|
||||||
|
|
||||||
@@ -123,84 +185,77 @@ mod tests {
|
|||||||
sonarr_data.main_tabs.tabs[0].route,
|
sonarr_data.main_tabs.tabs[0].route,
|
||||||
ActiveSonarrBlock::Series.into()
|
ActiveSonarrBlock::Series.into()
|
||||||
);
|
);
|
||||||
assert!(sonarr_data.main_tabs.tabs[0].contextual_help.is_some());
|
assert_some_eq_x!(
|
||||||
assert_eq!(
|
&sonarr_data.main_tabs.tabs[0].contextual_help,
|
||||||
sonarr_data.main_tabs.tabs[0].contextual_help.unwrap(),
|
|
||||||
&SERIES_CONTEXT_CLUES
|
&SERIES_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.main_tabs.tabs[0].config, None);
|
assert_none!(sonarr_data.main_tabs.tabs[0].config);
|
||||||
|
|
||||||
assert_str_eq!(sonarr_data.main_tabs.tabs[1].title, "Downloads");
|
assert_str_eq!(sonarr_data.main_tabs.tabs[1].title, "Downloads");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sonarr_data.main_tabs.tabs[1].route,
|
sonarr_data.main_tabs.tabs[1].route,
|
||||||
ActiveSonarrBlock::Downloads.into()
|
ActiveSonarrBlock::Downloads.into()
|
||||||
);
|
);
|
||||||
assert!(sonarr_data.main_tabs.tabs[1].contextual_help.is_some());
|
assert_some_eq_x!(
|
||||||
assert_eq!(
|
&sonarr_data.main_tabs.tabs[1].contextual_help,
|
||||||
sonarr_data.main_tabs.tabs[1].contextual_help.unwrap(),
|
|
||||||
&DOWNLOADS_CONTEXT_CLUES
|
&DOWNLOADS_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.main_tabs.tabs[1].config, None);
|
assert_none!(sonarr_data.main_tabs.tabs[1].config);
|
||||||
|
|
||||||
assert_str_eq!(sonarr_data.main_tabs.tabs[2].title, "Blocklist");
|
assert_str_eq!(sonarr_data.main_tabs.tabs[2].title, "Blocklist");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sonarr_data.main_tabs.tabs[2].route,
|
sonarr_data.main_tabs.tabs[2].route,
|
||||||
ActiveSonarrBlock::Blocklist.into()
|
ActiveSonarrBlock::Blocklist.into()
|
||||||
);
|
);
|
||||||
assert!(sonarr_data.main_tabs.tabs[2].contextual_help.is_some());
|
assert_some_eq_x!(
|
||||||
assert_eq!(
|
&sonarr_data.main_tabs.tabs[2].contextual_help,
|
||||||
sonarr_data.main_tabs.tabs[2].contextual_help.unwrap(),
|
|
||||||
&BLOCKLIST_CONTEXT_CLUES
|
&BLOCKLIST_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.main_tabs.tabs[2].config, None);
|
assert_none!(sonarr_data.main_tabs.tabs[2].config);
|
||||||
|
|
||||||
assert_str_eq!(sonarr_data.main_tabs.tabs[3].title, "History");
|
assert_str_eq!(sonarr_data.main_tabs.tabs[3].title, "History");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sonarr_data.main_tabs.tabs[3].route,
|
sonarr_data.main_tabs.tabs[3].route,
|
||||||
ActiveSonarrBlock::History.into()
|
ActiveSonarrBlock::History.into()
|
||||||
);
|
);
|
||||||
assert!(sonarr_data.main_tabs.tabs[3].contextual_help.is_some());
|
assert_some_eq_x!(
|
||||||
assert_eq!(
|
&sonarr_data.main_tabs.tabs[3].contextual_help,
|
||||||
sonarr_data.main_tabs.tabs[3].contextual_help.unwrap(),
|
|
||||||
&HISTORY_CONTEXT_CLUES
|
&HISTORY_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.main_tabs.tabs[3].config, None);
|
assert_none!(sonarr_data.main_tabs.tabs[3].config);
|
||||||
|
|
||||||
assert_str_eq!(sonarr_data.main_tabs.tabs[4].title, "Root Folders");
|
assert_str_eq!(sonarr_data.main_tabs.tabs[4].title, "Root Folders");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sonarr_data.main_tabs.tabs[4].route,
|
sonarr_data.main_tabs.tabs[4].route,
|
||||||
ActiveSonarrBlock::RootFolders.into()
|
ActiveSonarrBlock::RootFolders.into()
|
||||||
);
|
);
|
||||||
assert!(sonarr_data.main_tabs.tabs[4].contextual_help.is_some());
|
assert_some_eq_x!(
|
||||||
assert_eq!(
|
&sonarr_data.main_tabs.tabs[4].contextual_help,
|
||||||
sonarr_data.main_tabs.tabs[4].contextual_help.unwrap(),
|
|
||||||
&ROOT_FOLDERS_CONTEXT_CLUES
|
&ROOT_FOLDERS_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.main_tabs.tabs[4].config, None);
|
assert_none!(sonarr_data.main_tabs.tabs[4].config);
|
||||||
|
|
||||||
assert_str_eq!(sonarr_data.main_tabs.tabs[5].title, "Indexers");
|
assert_str_eq!(sonarr_data.main_tabs.tabs[5].title, "Indexers");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sonarr_data.main_tabs.tabs[5].route,
|
sonarr_data.main_tabs.tabs[5].route,
|
||||||
ActiveSonarrBlock::Indexers.into()
|
ActiveSonarrBlock::Indexers.into()
|
||||||
);
|
);
|
||||||
assert!(sonarr_data.main_tabs.tabs[5].contextual_help.is_some());
|
assert_some_eq_x!(
|
||||||
assert_eq!(
|
&sonarr_data.main_tabs.tabs[5].contextual_help,
|
||||||
sonarr_data.main_tabs.tabs[5].contextual_help.unwrap(),
|
|
||||||
&INDEXERS_CONTEXT_CLUES
|
&INDEXERS_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.main_tabs.tabs[5].config, None);
|
assert_none!(sonarr_data.main_tabs.tabs[5].config);
|
||||||
|
|
||||||
assert_str_eq!(sonarr_data.main_tabs.tabs[6].title, "System");
|
assert_str_eq!(sonarr_data.main_tabs.tabs[6].title, "System");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sonarr_data.main_tabs.tabs[6].route,
|
sonarr_data.main_tabs.tabs[6].route,
|
||||||
ActiveSonarrBlock::System.into()
|
ActiveSonarrBlock::System.into()
|
||||||
);
|
);
|
||||||
assert!(sonarr_data.main_tabs.tabs[6].contextual_help.is_some());
|
assert_some_eq_x!(
|
||||||
assert_eq!(
|
&sonarr_data.main_tabs.tabs[6].contextual_help,
|
||||||
sonarr_data.main_tabs.tabs[6].contextual_help.unwrap(),
|
|
||||||
&SYSTEM_CONTEXT_CLUES
|
&SYSTEM_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.main_tabs.tabs[6].config, None);
|
assert_none!(sonarr_data.main_tabs.tabs[6].config);
|
||||||
|
|
||||||
assert_eq!(sonarr_data.series_info_tabs.tabs.len(), 2);
|
assert_eq!(sonarr_data.series_info_tabs.tabs.len(), 2);
|
||||||
|
|
||||||
@@ -209,36 +264,22 @@ mod tests {
|
|||||||
sonarr_data.series_info_tabs.tabs[0].route,
|
sonarr_data.series_info_tabs.tabs[0].route,
|
||||||
ActiveSonarrBlock::SeriesDetails.into()
|
ActiveSonarrBlock::SeriesDetails.into()
|
||||||
);
|
);
|
||||||
assert!(
|
assert_some_eq_x!(
|
||||||
sonarr_data.series_info_tabs.tabs[0]
|
&sonarr_data.series_info_tabs.tabs[0].contextual_help,
|
||||||
.contextual_help
|
|
||||||
.is_some()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
sonarr_data.series_info_tabs.tabs[0]
|
|
||||||
.contextual_help
|
|
||||||
.unwrap(),
|
|
||||||
&SERIES_DETAILS_CONTEXT_CLUES
|
&SERIES_DETAILS_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.series_info_tabs.tabs[0].config, None);
|
assert_none!(sonarr_data.series_info_tabs.tabs[0].config);
|
||||||
|
|
||||||
assert_str_eq!(sonarr_data.series_info_tabs.tabs[1].title, "History");
|
assert_str_eq!(sonarr_data.series_info_tabs.tabs[1].title, "History");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sonarr_data.series_info_tabs.tabs[1].route,
|
sonarr_data.series_info_tabs.tabs[1].route,
|
||||||
ActiveSonarrBlock::SeriesHistory.into()
|
ActiveSonarrBlock::SeriesHistory.into()
|
||||||
);
|
);
|
||||||
assert!(
|
assert_some_eq_x!(
|
||||||
sonarr_data.series_info_tabs.tabs[1]
|
&sonarr_data.series_info_tabs.tabs[1].contextual_help,
|
||||||
.contextual_help
|
|
||||||
.is_some()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
sonarr_data.series_info_tabs.tabs[1]
|
|
||||||
.contextual_help
|
|
||||||
.unwrap(),
|
|
||||||
&SERIES_HISTORY_CONTEXT_CLUES
|
&SERIES_HISTORY_CONTEXT_CLUES
|
||||||
);
|
);
|
||||||
assert_eq!(sonarr_data.series_info_tabs.tabs[1].config, None);
|
assert_none!(sonarr_data.series_info_tabs.tabs[1].config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,9 +174,25 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_filtered_items(&mut self, filtered_items: Vec<T>) {
|
pub fn set_filtered_items(&mut self, filtered_items: Vec<T>) {
|
||||||
|
let items_len = filtered_items.len();
|
||||||
self.filtered_items = Some(filtered_items);
|
self.filtered_items = Some(filtered_items);
|
||||||
|
|
||||||
|
let preserved_selection = self
|
||||||
|
.filtered_state
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|state| state.selected())
|
||||||
|
.map_or(0, |i| {
|
||||||
|
if i > 0 && i < items_len {
|
||||||
|
i
|
||||||
|
} else if i >= items_len && items_len > 0 {
|
||||||
|
items_len - 1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut filtered_state: TableState = Default::default();
|
let mut filtered_state: TableState = Default::default();
|
||||||
filtered_state.select(Some(0));
|
filtered_state.select(Some(preserved_selection));
|
||||||
self.filtered_state = Some(filtered_state);
|
self.filtered_state = Some(filtered_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -390,6 +390,47 @@ mod tests {
|
|||||||
assert_some_eq_x!(&filtered_stateful_table.filtered_items, &filtered_items_vec);
|
assert_some_eq_x!(&filtered_stateful_table.filtered_items, &filtered_items_vec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_table_set_filtered_items_preserves_selection() {
|
||||||
|
let filtered_items_vec = vec!["Test 1", "Test 2", "Test 3"];
|
||||||
|
let mut filtered_stateful_table: StatefulTable<&str> = StatefulTable::default();
|
||||||
|
|
||||||
|
filtered_stateful_table.set_filtered_items(filtered_items_vec.clone());
|
||||||
|
filtered_stateful_table
|
||||||
|
.filtered_state
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.select(Some(1));
|
||||||
|
|
||||||
|
filtered_stateful_table.set_filtered_items(filtered_items_vec.clone());
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
filtered_stateful_table
|
||||||
|
.filtered_state
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.selected(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
filtered_stateful_table
|
||||||
|
.filtered_state
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.select(Some(5));
|
||||||
|
|
||||||
|
filtered_stateful_table.set_filtered_items(filtered_items_vec);
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
filtered_stateful_table
|
||||||
|
.filtered_state
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.selected(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stateful_table_current_selection() {
|
fn test_stateful_table_current_selection() {
|
||||||
let mut stateful_table = create_test_stateful_table();
|
let mut stateful_table = create_test_stateful_table();
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::models::lidarr_models::{DownloadsResponse, LidarrSerdeable};
|
||||||
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
|
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_downloads_event() {
|
||||||
|
let downloads_json = json!({
|
||||||
|
"records": [{
|
||||||
|
"title": "Test Album",
|
||||||
|
"status": "downloading",
|
||||||
|
"id": 1,
|
||||||
|
"size": 100.0,
|
||||||
|
"sizeleft": 50.0,
|
||||||
|
"indexer": "test-indexer"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(downloads_json)
|
||||||
|
.query("pageSize=500")
|
||||||
|
.build_for(LidarrEvent::GetDownloads(500))
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::GetDownloads(500))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else {
|
||||||
|
panic!("Expected DownloadsResponse");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(downloads_response, response);
|
||||||
|
assert!(!app.lock().await.data.lidarr_data.downloads.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::models::lidarr_models::DownloadsResponse;
|
||||||
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
|
use crate::network::{Network, RequestMethod};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "lidarr_downloads_network_tests.rs"]
|
||||||
|
mod lidarr_downloads_network_tests;
|
||||||
|
|
||||||
|
impl Network<'_, '_> {
|
||||||
|
pub(in crate::network::lidarr_network) async fn get_lidarr_downloads(
|
||||||
|
&mut self,
|
||||||
|
count: u64,
|
||||||
|
) -> Result<DownloadsResponse> {
|
||||||
|
info!("Fetching Lidarr downloads");
|
||||||
|
let event = LidarrEvent::GetDownloads(count);
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
event,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
None,
|
||||||
|
Some(format!("pageSize={count}")),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| {
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.downloads
|
||||||
|
.set_items(queue_response.records);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::models::lidarr_models::{
|
||||||
|
AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams, LidarrSerdeable,
|
||||||
|
NewItemMonitorType,
|
||||||
|
};
|
||||||
|
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||||
|
use crate::network::NetworkResource;
|
||||||
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
|
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||||
|
ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON,
|
||||||
|
};
|
||||||
|
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||||
|
use bimap::BiMap;
|
||||||
|
use mockito::Matcher;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_list_artists_event() {
|
||||||
|
let artists_json = json!([{
|
||||||
|
"id": 1,
|
||||||
|
"artistName": "Test Artist",
|
||||||
|
"foreignArtistId": "test-foreign-id",
|
||||||
|
"status": "continuing",
|
||||||
|
"path": "/music/test-artist",
|
||||||
|
"qualityProfileId": 1,
|
||||||
|
"metadataProfileId": 1,
|
||||||
|
"monitored": true,
|
||||||
|
"monitorNewItems": "all",
|
||||||
|
"genres": [],
|
||||||
|
"tags": [],
|
||||||
|
"added": "2023-01-01T00:00:00Z"
|
||||||
|
}]);
|
||||||
|
let response: Vec<Artist> = serde_json::from_value(artists_json.clone()).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(artists_json)
|
||||||
|
.build_for(LidarrEvent::ListArtists)
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network.handle_lidarr_event(LidarrEvent::ListArtists).await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::Artists(artists) = result.unwrap() else {
|
||||||
|
panic!("Expected Artists");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(artists, response);
|
||||||
|
assert!(!app.lock().await.data.lidarr_data.artists.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_delete_artist_event() {
|
||||||
|
let delete_artist_params = DeleteArtistParams {
|
||||||
|
id: 1,
|
||||||
|
delete_files: true,
|
||||||
|
add_import_list_exclusion: true,
|
||||||
|
};
|
||||||
|
let (async_server, app, _server) = MockServarrApi::delete()
|
||||||
|
.path("/1")
|
||||||
|
.query("deleteFiles=true&addImportListExclusion=true")
|
||||||
|
.build_for(LidarrEvent::DeleteArtist(delete_artist_params.clone()))
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
network
|
||||||
|
.handle_lidarr_event(LidarrEvent::DeleteArtist(delete_artist_params))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
async_server.assert_async().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_artist_details_event() {
|
||||||
|
let expected_artist: Artist = serde_json::from_str(ARTIST_JSON).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(serde_json::from_str(ARTIST_JSON).unwrap())
|
||||||
|
.path("/1")
|
||||||
|
.build_for(LidarrEvent::GetArtistDetails(1))
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::GetArtistDetails(1))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::Artist(artist) = result.unwrap() else {
|
||||||
|
panic!("Expected Artist");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(artist, expected_artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_toggle_artist_monitoring_event() {
|
||||||
|
let artist_json = json!({
|
||||||
|
"id": 1,
|
||||||
|
"artistName": "Test Artist",
|
||||||
|
"foreignArtistId": "test-foreign-id",
|
||||||
|
"status": "continuing",
|
||||||
|
"path": "/music/test-artist",
|
||||||
|
"qualityProfileId": 1,
|
||||||
|
"metadataProfileId": 1,
|
||||||
|
"monitored": true,
|
||||||
|
"monitorNewItems": "all",
|
||||||
|
"genres": [],
|
||||||
|
"tags": [],
|
||||||
|
"added": "2023-01-01T00:00:00Z"
|
||||||
|
});
|
||||||
|
let mut expected_body = artist_json.clone();
|
||||||
|
*expected_body.get_mut("monitored").unwrap() = json!(false);
|
||||||
|
let (get_mock, app, mut server) = MockServarrApi::get()
|
||||||
|
.returns(artist_json)
|
||||||
|
.path("/1")
|
||||||
|
.build_for(LidarrEvent::GetArtistDetails(1))
|
||||||
|
.await;
|
||||||
|
let put_mock = server
|
||||||
|
.mock("PUT", "/api/v1/artist/1")
|
||||||
|
.match_body(Matcher::Json(expected_body))
|
||||||
|
.match_header("X-Api-Key", "test1234")
|
||||||
|
.with_status(202)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
network
|
||||||
|
.handle_lidarr_event(LidarrEvent::ToggleArtistMonitoring(1))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
get_mock.assert_async().await;
|
||||||
|
put_mock.assert_async().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_update_all_artists_event() {
|
||||||
|
let (mock, app, _server) = MockServarrApi::post()
|
||||||
|
.with_request_body(json!({
|
||||||
|
"name": "RefreshArtist"
|
||||||
|
}))
|
||||||
|
.returns(json!({}))
|
||||||
|
.build_for(LidarrEvent::UpdateAllArtists)
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
network
|
||||||
|
.handle_lidarr_event(LidarrEvent::UpdateAllArtists)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_artist_event() {
|
||||||
|
let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap();
|
||||||
|
*expected_body.get_mut("monitored").unwrap() = json!(false);
|
||||||
|
*expected_body.get_mut("monitorNewItems").unwrap() = json!("none");
|
||||||
|
*expected_body.get_mut("qualityProfileId").unwrap() = json!(1111);
|
||||||
|
*expected_body.get_mut("metadataProfileId").unwrap() = json!(2222);
|
||||||
|
*expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path");
|
||||||
|
*expected_body.get_mut("tags").unwrap() = json!([1, 2]);
|
||||||
|
let edit_artist_params = EditArtistParams {
|
||||||
|
artist_id: 1,
|
||||||
|
monitored: Some(false),
|
||||||
|
monitor_new_items: Some(NewItemMonitorType::None),
|
||||||
|
quality_profile_id: Some(1111),
|
||||||
|
metadata_profile_id: Some(2222),
|
||||||
|
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||||
|
tag_input_string: Some("usenet, testing".to_owned()),
|
||||||
|
..EditArtistParams::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (async_details_server, app, mut server) = MockServarrApi::get()
|
||||||
|
.returns(serde_json::from_str(ARTIST_JSON).unwrap())
|
||||||
|
.path("/1")
|
||||||
|
.build_for(LidarrEvent::GetArtistDetails(1))
|
||||||
|
.await;
|
||||||
|
let async_edit_server = server
|
||||||
|
.mock(
|
||||||
|
"PUT",
|
||||||
|
format!(
|
||||||
|
"/api/v1{}/1",
|
||||||
|
LidarrEvent::EditArtist(edit_artist_params.clone()).resource()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.with_status(202)
|
||||||
|
.match_header("X-Api-Key", "test1234")
|
||||||
|
.match_body(Matcher::Json(expected_body))
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
app.lock().await.data.lidarr_data.tags_map =
|
||||||
|
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
network
|
||||||
|
.handle_lidarr_event(LidarrEvent::EditArtist(edit_artist_params))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
async_details_server.assert_async().await;
|
||||||
|
async_edit_server.assert_async().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_artist_event_does_not_overwrite_tag_ids_vec_when_tag_input_string_is_none()
|
||||||
|
{
|
||||||
|
let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap();
|
||||||
|
*expected_body.get_mut("monitored").unwrap() = json!(false);
|
||||||
|
*expected_body.get_mut("monitorNewItems").unwrap() = json!("none");
|
||||||
|
*expected_body.get_mut("qualityProfileId").unwrap() = json!(1111);
|
||||||
|
*expected_body.get_mut("metadataProfileId").unwrap() = json!(2222);
|
||||||
|
*expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path");
|
||||||
|
*expected_body.get_mut("tags").unwrap() = json!([1, 2]);
|
||||||
|
let edit_artist_params = EditArtistParams {
|
||||||
|
artist_id: 1,
|
||||||
|
monitored: Some(false),
|
||||||
|
monitor_new_items: Some(NewItemMonitorType::None),
|
||||||
|
quality_profile_id: Some(1111),
|
||||||
|
metadata_profile_id: Some(2222),
|
||||||
|
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||||
|
tags: Some(vec![1, 2]),
|
||||||
|
..EditArtistParams::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (async_details_server, app, mut server) = MockServarrApi::get()
|
||||||
|
.returns(serde_json::from_str(ARTIST_JSON).unwrap())
|
||||||
|
.path("/1")
|
||||||
|
.build_for(LidarrEvent::GetArtistDetails(1))
|
||||||
|
.await;
|
||||||
|
let async_edit_server = server
|
||||||
|
.mock(
|
||||||
|
"PUT",
|
||||||
|
format!(
|
||||||
|
"/api/v1{}/1",
|
||||||
|
LidarrEvent::EditArtist(edit_artist_params.clone()).resource()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.with_status(202)
|
||||||
|
.match_header("X-Api-Key", "test1234")
|
||||||
|
.match_body(Matcher::Json(expected_body))
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
app.lock().await.data.lidarr_data.tags_map =
|
||||||
|
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
network
|
||||||
|
.handle_lidarr_event(LidarrEvent::EditArtist(edit_artist_params))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
async_details_server.assert_async().await;
|
||||||
|
async_edit_server.assert_async().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_artist_event_defaults_to_previous_values() {
|
||||||
|
let edit_artist_params = EditArtistParams {
|
||||||
|
artist_id: 1,
|
||||||
|
..EditArtistParams::default()
|
||||||
|
};
|
||||||
|
let expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap();
|
||||||
|
let (async_details_server, app, mut server) = MockServarrApi::get()
|
||||||
|
.returns(serde_json::from_str(ARTIST_JSON).unwrap())
|
||||||
|
.path("/1")
|
||||||
|
.build_for(LidarrEvent::GetArtistDetails(1))
|
||||||
|
.await;
|
||||||
|
let async_edit_server = server
|
||||||
|
.mock(
|
||||||
|
"PUT",
|
||||||
|
format!(
|
||||||
|
"/api/v1{}/1",
|
||||||
|
LidarrEvent::EditArtist(edit_artist_params.clone()).resource()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.with_status(202)
|
||||||
|
.match_header("X-Api-Key", "test1234")
|
||||||
|
.match_body(Matcher::Json(expected_body))
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
network
|
||||||
|
.handle_lidarr_event(LidarrEvent::EditArtist(edit_artist_params))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
async_details_server.assert_async().await;
|
||||||
|
async_edit_server.assert_async().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_artist_event_returns_empty_tags_vec_when_clear_tags_is_true() {
|
||||||
|
let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap();
|
||||||
|
*expected_body.get_mut("tags").unwrap() = json!([]);
|
||||||
|
|
||||||
|
let (async_details_server, app, mut server) = MockServarrApi::get()
|
||||||
|
.returns(serde_json::from_str(ARTIST_JSON).unwrap())
|
||||||
|
.path("/1")
|
||||||
|
.build_for(LidarrEvent::GetArtistDetails(1))
|
||||||
|
.await;
|
||||||
|
let edit_artist_params = EditArtistParams {
|
||||||
|
artist_id: 1,
|
||||||
|
clear_tags: true,
|
||||||
|
..EditArtistParams::default()
|
||||||
|
};
|
||||||
|
let async_edit_server = server
|
||||||
|
.mock(
|
||||||
|
"PUT",
|
||||||
|
format!(
|
||||||
|
"/api/v1{}/1",
|
||||||
|
LidarrEvent::EditArtist(edit_artist_params.clone()).resource()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.with_status(202)
|
||||||
|
.match_header("X-Api-Key", "test1234")
|
||||||
|
.match_body(Matcher::Json(expected_body))
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
network
|
||||||
|
.handle_lidarr_event(LidarrEvent::EditArtist(edit_artist_params))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
async_details_server.assert_async().await;
|
||||||
|
async_edit_server.assert_async().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_search_new_artist_event() {
|
||||||
|
let search_results_json =
|
||||||
|
json!([serde_json::from_str::<Value>(ADD_ARTIST_SEARCH_RESULT_JSON).unwrap()]);
|
||||||
|
let expected_results: Vec<AddArtistSearchResult> =
|
||||||
|
serde_json::from_value(search_results_json.clone()).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(search_results_json)
|
||||||
|
.query("term=test%20artist")
|
||||||
|
.build_for(LidarrEvent::SearchNewArtist("test artist".to_owned()))
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::SearchNewArtist("test artist".to_owned()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::AddArtistSearchResults(search_results) = result.unwrap() else {
|
||||||
|
panic!("Expected AddArtistSearchResults");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(search_results, expected_results);
|
||||||
|
assert_some!(&app.lock().await.data.lidarr_data.add_searched_artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_search_new_artist_event_navigates_to_empty_results_when_empty() {
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(json!([]))
|
||||||
|
.query("term=nonexistent")
|
||||||
|
.build_for(LidarrEvent::SearchNewArtist("nonexistent".to_owned()))
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
app
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::SearchNewArtist("nonexistent".to_owned()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
assert_ok!(result);
|
||||||
|
let app = app.lock().await;
|
||||||
|
assert_none!(&app.data.lidarr_data.add_searched_artists);
|
||||||
|
assert_eq!(
|
||||||
|
app.get_current_route(),
|
||||||
|
ActiveLidarrBlock::AddArtistEmptySearchResults.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_search_new_artist_event_sets_empty_table_on_api_error() {
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.status(500)
|
||||||
|
.query("term=nonexistent")
|
||||||
|
.build_for(LidarrEvent::SearchNewArtist("nonexistent".to_owned()))
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::SearchNewArtist("nonexistent".to_owned()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
assert_err!(result);
|
||||||
|
let app = app.lock().await;
|
||||||
|
assert_some!(&app.data.lidarr_data.add_searched_artists);
|
||||||
|
assert_is_empty!(app.data.lidarr_data.add_searched_artists.as_ref().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::models::Route;
|
||||||
|
use crate::models::lidarr_models::{
|
||||||
|
AddArtistSearchResult, Artist, DeleteArtistParams, EditArtistParams,
|
||||||
|
};
|
||||||
|
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||||
|
use crate::models::servarr_models::CommandBody;
|
||||||
|
use crate::models::stateful_table::StatefulTable;
|
||||||
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
|
use crate::network::{Network, RequestMethod};
|
||||||
|
use urlencoding::encode;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "lidarr_library_network_tests.rs"]
|
||||||
|
mod lidarr_library_network_tests;
|
||||||
|
|
||||||
|
impl Network<'_, '_> {
|
||||||
|
pub(in crate::network::lidarr_network) async fn delete_artist(
|
||||||
|
&mut self,
|
||||||
|
delete_artist_params: DeleteArtistParams,
|
||||||
|
) -> Result<()> {
|
||||||
|
let event = LidarrEvent::DeleteArtist(DeleteArtistParams::default());
|
||||||
|
let DeleteArtistParams {
|
||||||
|
id,
|
||||||
|
delete_files,
|
||||||
|
add_import_list_exclusion,
|
||||||
|
} = delete_artist_params;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Deleting Lidarr artist with ID: {id} with deleteFiles={delete_files} and addImportListExclusion={add_import_list_exclusion}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
event,
|
||||||
|
RequestMethod::Delete,
|
||||||
|
None::<()>,
|
||||||
|
Some(format!("/{id}")),
|
||||||
|
Some(format!(
|
||||||
|
"deleteFiles={delete_files}&addImportListExclusion={add_import_list_exclusion}"
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::network::lidarr_network) async fn list_artists(&mut self) -> Result<Vec<Artist>> {
|
||||||
|
info!("Fetching Lidarr artists");
|
||||||
|
let event = LidarrEvent::ListArtists;
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), Vec<Artist>>(request_props, |mut artists_vec, mut app| {
|
||||||
|
if !matches!(
|
||||||
|
app.get_current_route(),
|
||||||
|
Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _)
|
||||||
|
) {
|
||||||
|
artists_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
app.data.lidarr_data.artists.set_items(artists_vec);
|
||||||
|
app.data.lidarr_data.artists.apply_sorting_toggle(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::network::lidarr_network) async fn get_artist_details(
|
||||||
|
&mut self,
|
||||||
|
artist_id: i64,
|
||||||
|
) -> Result<Artist> {
|
||||||
|
info!("Fetching details for Lidarr artist with ID: {artist_id}");
|
||||||
|
let event = LidarrEvent::GetArtistDetails(artist_id);
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
event,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
Some(format!("/{artist_id}")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), Artist>(request_props, |_, _| ())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::network::lidarr_network) async fn toggle_artist_monitoring(
|
||||||
|
&mut self,
|
||||||
|
artist_id: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let event = LidarrEvent::ToggleArtistMonitoring(artist_id);
|
||||||
|
|
||||||
|
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
|
||||||
|
info!("Toggling artist monitoring for artist with ID: {artist_id}");
|
||||||
|
info!("Fetching artist details for artist with ID: {artist_id}");
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
detail_event,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
Some(format!("/{artist_id}")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut response = String::new();
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
|
||||||
|
response = detailed_artist_body.to_string()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("Constructing toggle artist monitoring body");
|
||||||
|
|
||||||
|
match serde_json::from_str::<Value>(&response) {
|
||||||
|
Ok(mut detailed_artist_body) => {
|
||||||
|
let monitored = detailed_artist_body
|
||||||
|
.get("monitored")
|
||||||
|
.unwrap()
|
||||||
|
.as_bool()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
*detailed_artist_body.get_mut("monitored").unwrap() = json!(!monitored);
|
||||||
|
|
||||||
|
debug!("Toggle artist monitoring body: {detailed_artist_body:?}");
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
event,
|
||||||
|
RequestMethod::Put,
|
||||||
|
Some(detailed_artist_body),
|
||||||
|
Some(format!("/{artist_id}")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Request for detailed artist body was interrupted");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::network::lidarr_network) async fn update_all_artists(&mut self) -> Result<Value> {
|
||||||
|
info!("Updating all artists");
|
||||||
|
let event = LidarrEvent::UpdateAllArtists;
|
||||||
|
let body = CommandBody {
|
||||||
|
name: "RefreshArtist".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::network::lidarr_network) async fn search_artist(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
) -> Result<Vec<AddArtistSearchResult>> {
|
||||||
|
info!("Searching for artist: {query}");
|
||||||
|
let event = LidarrEvent::SearchNewArtist(String::new());
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
event,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
None,
|
||||||
|
Some(format!("term={}", encode(&query))),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.handle_request::<(), Vec<AddArtistSearchResult>>(request_props, |artist_vec, mut app| {
|
||||||
|
if artist_vec.is_empty() {
|
||||||
|
app.pop_and_push_navigation_stack(ActiveLidarrBlock::AddArtistEmptySearchResults.into());
|
||||||
|
} else if let Some(add_searched_artists) =
|
||||||
|
app.data.lidarr_data.add_searched_artists.as_mut()
|
||||||
|
{
|
||||||
|
add_searched_artists.set_items(artist_vec);
|
||||||
|
} else {
|
||||||
|
let mut add_searched_artists = StatefulTable::default();
|
||||||
|
add_searched_artists.set_items(artist_vec);
|
||||||
|
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if result.is_err() {
|
||||||
|
self.app.lock().await.data.lidarr_data.add_searched_artists = Some(StatefulTable::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::network::lidarr_network) async fn edit_artist(
|
||||||
|
&mut self,
|
||||||
|
mut edit_artist_params: EditArtistParams,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("Editing Lidarr artist");
|
||||||
|
if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() {
|
||||||
|
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
|
||||||
|
edit_artist_params.tags = Some(tag_ids_vec);
|
||||||
|
}
|
||||||
|
let artist_id = edit_artist_params.artist_id;
|
||||||
|
let detail_event = LidarrEvent::GetArtistDetails(artist_id);
|
||||||
|
let event = LidarrEvent::EditArtist(EditArtistParams::default());
|
||||||
|
info!("Fetching artist details for artist with ID: {artist_id}");
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
detail_event,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
Some(format!("/{artist_id}")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut response = String::new();
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), Value>(request_props, |detailed_artist_body, _| {
|
||||||
|
response = detailed_artist_body.to_string()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("Constructing edit artist body");
|
||||||
|
|
||||||
|
let mut detailed_artist_body: Value = serde_json::from_str(&response)?;
|
||||||
|
let (
|
||||||
|
monitored,
|
||||||
|
monitor_new_items,
|
||||||
|
quality_profile_id,
|
||||||
|
metadata_profile_id,
|
||||||
|
root_folder_path,
|
||||||
|
tags,
|
||||||
|
) = {
|
||||||
|
let monitored = edit_artist_params.monitored.unwrap_or(
|
||||||
|
detailed_artist_body["monitored"]
|
||||||
|
.as_bool()
|
||||||
|
.expect("Unable to deserialize 'monitored'"),
|
||||||
|
);
|
||||||
|
let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| {
|
||||||
|
serde_json::from_value(detailed_artist_body["monitorNewItems"].clone())
|
||||||
|
.expect("Unable to deserialize 'monitorNewItems'")
|
||||||
|
});
|
||||||
|
let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| {
|
||||||
|
detailed_artist_body["qualityProfileId"]
|
||||||
|
.as_i64()
|
||||||
|
.expect("Unable to deserialize 'qualityProfileId'")
|
||||||
|
});
|
||||||
|
let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| {
|
||||||
|
detailed_artist_body["metadataProfileId"]
|
||||||
|
.as_i64()
|
||||||
|
.expect("Unable to deserialize 'metadataProfileId'")
|
||||||
|
});
|
||||||
|
let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| {
|
||||||
|
detailed_artist_body["path"]
|
||||||
|
.as_str()
|
||||||
|
.expect("Unable to deserialize 'path'")
|
||||||
|
.to_owned()
|
||||||
|
});
|
||||||
|
let tags = if edit_artist_params.clear_tags {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
edit_artist_params.tags.unwrap_or(
|
||||||
|
detailed_artist_body["tags"]
|
||||||
|
.as_array()
|
||||||
|
.expect("Unable to deserialize 'tags'")
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
monitored,
|
||||||
|
monitor_new_items,
|
||||||
|
quality_profile_id,
|
||||||
|
metadata_profile_id,
|
||||||
|
root_folder_path,
|
||||||
|
tags,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
*detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored);
|
||||||
|
*detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items);
|
||||||
|
*detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
|
||||||
|
*detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id);
|
||||||
|
*detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path);
|
||||||
|
*detailed_artist_body.get_mut("tags").unwrap() = json!(tags);
|
||||||
|
|
||||||
|
debug!("Edit artist body: {detailed_artist_body:?}");
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
event,
|
||||||
|
RequestMethod::Put,
|
||||||
|
Some(detailed_artist_body),
|
||||||
|
Some(format!("/{artist_id}")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub mod test_utils {
|
||||||
|
use crate::models::HorizontallyScrollableText;
|
||||||
|
use crate::models::lidarr_models::{
|
||||||
|
AddArtistSearchResult, Artist, ArtistStatistics, ArtistStatus, DownloadRecord, DownloadStatus,
|
||||||
|
DownloadsResponse, EditArtistParams, Member, MetadataProfile, NewItemMonitorType, Ratings,
|
||||||
|
SystemStatus,
|
||||||
|
};
|
||||||
|
use crate::models::servarr_models::{QualityProfile, RootFolder, Tag};
|
||||||
|
use bimap::BiMap;
|
||||||
|
use chrono::DateTime;
|
||||||
|
use serde_json::Number;
|
||||||
|
|
||||||
|
pub const ADD_ARTIST_SEARCH_RESULT_JSON: &str = r#"{
|
||||||
|
"foreignArtistId": "test-foreign-id",
|
||||||
|
"artistName": "Test Artist",
|
||||||
|
"status": "continuing",
|
||||||
|
"overview": "some interesting description of the artist",
|
||||||
|
"artistType": "Person",
|
||||||
|
"disambiguation": "American pianist",
|
||||||
|
"genres": ["soundtrack"],
|
||||||
|
"ratings": { "votes": 15, "value": 8.4 }
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
pub const ARTIST_JSON: &str = r#"{
|
||||||
|
"id": 1,
|
||||||
|
"artistName": "Test Artist",
|
||||||
|
"foreignArtistId": "test-foreign-id",
|
||||||
|
"status": "continuing",
|
||||||
|
"overview": "some interesting description of the artist",
|
||||||
|
"artistType": "Person",
|
||||||
|
"disambiguation": "American pianist",
|
||||||
|
"path": "/music/test-artist",
|
||||||
|
"members": [{"name": "alex", "instrument": "piano"}],
|
||||||
|
"qualityProfileId": 1,
|
||||||
|
"metadataProfileId": 1,
|
||||||
|
"monitored": true,
|
||||||
|
"monitorNewItems": "all",
|
||||||
|
"genres": ["soundtrack"],
|
||||||
|
"tags": [1],
|
||||||
|
"added": "2023-01-01T00:00:00Z",
|
||||||
|
"ratings": { "votes": 15, "value": 8.4 },
|
||||||
|
"statistics": {
|
||||||
|
"albumCount": 1,
|
||||||
|
"trackFileCount": 15,
|
||||||
|
"trackCount": 15,
|
||||||
|
"totalTrackCount": 15,
|
||||||
|
"sizeOnDisk": 12345,
|
||||||
|
"percentOfTracks": 99.9
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
pub fn member() -> Member {
|
||||||
|
Member {
|
||||||
|
name: Some("alex".to_owned()),
|
||||||
|
instrument: Some("piano".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ratings() -> Ratings {
|
||||||
|
Ratings {
|
||||||
|
votes: 15,
|
||||||
|
value: 8.4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn artist_statistics() -> ArtistStatistics {
|
||||||
|
ArtistStatistics {
|
||||||
|
album_count: 1,
|
||||||
|
track_file_count: 15,
|
||||||
|
track_count: 15,
|
||||||
|
total_track_count: 15,
|
||||||
|
size_on_disk: 12345,
|
||||||
|
percent_of_tracks: 99.9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn artist() -> Artist {
|
||||||
|
Artist {
|
||||||
|
id: 1,
|
||||||
|
artist_name: "Alex".into(),
|
||||||
|
foreign_artist_id: "test-foreign-id".to_owned(),
|
||||||
|
status: ArtistStatus::Continuing,
|
||||||
|
overview: Some("some interesting description of the artist".to_owned()),
|
||||||
|
artist_type: Some("Person".to_owned()),
|
||||||
|
disambiguation: Some("American pianist".to_owned()),
|
||||||
|
members: Some(vec![member()]),
|
||||||
|
path: "/nfs/music/test-artist".to_owned(),
|
||||||
|
quality_profile_id: quality_profile().id,
|
||||||
|
metadata_profile_id: metadata_profile().id,
|
||||||
|
monitored: true,
|
||||||
|
monitor_new_items: NewItemMonitorType::All,
|
||||||
|
genres: vec!["soundtrack".to_owned()],
|
||||||
|
tags: vec![Number::from(tag().id)],
|
||||||
|
added: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
|
||||||
|
ratings: Some(ratings()),
|
||||||
|
statistics: Some(artist_statistics()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quality_profile() -> QualityProfile {
|
||||||
|
QualityProfile {
|
||||||
|
id: 1,
|
||||||
|
name: "Lossless".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quality_profile_map() -> BiMap<i64, String> {
|
||||||
|
let quality_profile = quality_profile();
|
||||||
|
BiMap::from_iter(vec![(quality_profile.id, quality_profile.name)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata_profile() -> MetadataProfile {
|
||||||
|
MetadataProfile {
|
||||||
|
id: 1,
|
||||||
|
name: "Standard".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata_profile_map() -> BiMap<i64, String> {
|
||||||
|
let metadata_profile = metadata_profile();
|
||||||
|
BiMap::from_iter(vec![(metadata_profile.id, metadata_profile.name)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag() -> Tag {
|
||||||
|
Tag {
|
||||||
|
id: 1,
|
||||||
|
label: "alex".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tags_map() -> BiMap<i64, String> {
|
||||||
|
let tag = tag();
|
||||||
|
BiMap::from_iter(vec![(tag.id, tag.label)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_record() -> DownloadRecord {
|
||||||
|
DownloadRecord {
|
||||||
|
title: "Test download title".to_owned(),
|
||||||
|
status: DownloadStatus::Downloading,
|
||||||
|
id: 1,
|
||||||
|
album_id: Some(Number::from(1i64)),
|
||||||
|
artist_id: Some(Number::from(1i64)),
|
||||||
|
size: 3543348019f64,
|
||||||
|
sizeleft: 1771674009f64,
|
||||||
|
output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")),
|
||||||
|
indexer: "kickass torrents".to_owned(),
|
||||||
|
download_client: Some("transmission".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn downloads_response() -> DownloadsResponse {
|
||||||
|
DownloadsResponse {
|
||||||
|
records: vec![download_record()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system_status() -> SystemStatus {
|
||||||
|
SystemStatus {
|
||||||
|
version: "1.0".to_owned(),
|
||||||
|
start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root_folder() -> RootFolder {
|
||||||
|
RootFolder {
|
||||||
|
id: 1,
|
||||||
|
path: "/nfs".to_owned(),
|
||||||
|
accessible: true,
|
||||||
|
free_space: 219902325555200,
|
||||||
|
unmapped_folders: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn edit_artist_params() -> EditArtistParams {
|
||||||
|
EditArtistParams {
|
||||||
|
artist_id: artist().id,
|
||||||
|
monitored: Some(true),
|
||||||
|
monitor_new_items: Some(NewItemMonitorType::All),
|
||||||
|
quality_profile_id: Some(quality_profile().id),
|
||||||
|
metadata_profile_id: Some(metadata_profile().id),
|
||||||
|
root_folder_path: Some("/nfs/music/test-artist".to_owned()),
|
||||||
|
tags: Some(vec![tag().id]),
|
||||||
|
tag_input_string: Some("alex".to_owned()),
|
||||||
|
clear_tags: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_artist_search_result() -> AddArtistSearchResult {
|
||||||
|
AddArtistSearchResult {
|
||||||
|
foreign_artist_id: "test-foreign-id".to_owned(),
|
||||||
|
artist_name: "Test Artist".into(),
|
||||||
|
status: ArtistStatus::Continuing,
|
||||||
|
overview: Some("some interesting description of the artist".to_owned()),
|
||||||
|
artist_type: Some("Person".to_owned()),
|
||||||
|
disambiguation: Some("American pianist".to_owned()),
|
||||||
|
genres: vec!["soundtrack".to_owned()],
|
||||||
|
ratings: Some(ratings()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::models::lidarr_models::{LidarrSerdeable, MetadataProfile};
|
||||||
|
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||||
|
use crate::models::servarr_models::{QualityProfile, Tag};
|
||||||
|
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||||
|
use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent};
|
||||||
|
use bimap::BiMap;
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use rstest::rstest;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_resource_artist(
|
||||||
|
#[values(
|
||||||
|
LidarrEvent::GetArtistDetails(0),
|
||||||
|
LidarrEvent::ListArtists,
|
||||||
|
LidarrEvent::ToggleArtistMonitoring(0)
|
||||||
|
)]
|
||||||
|
event: LidarrEvent,
|
||||||
|
) {
|
||||||
|
assert_str_eq!(event.resource(), "/artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_resource_tag(
|
||||||
|
#[values(
|
||||||
|
LidarrEvent::AddTag(String::new()),
|
||||||
|
LidarrEvent::DeleteTag(0),
|
||||||
|
LidarrEvent::GetTags
|
||||||
|
)]
|
||||||
|
event: LidarrEvent,
|
||||||
|
) {
|
||||||
|
assert_str_eq!(event.resource(), "/tag");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_resource_config(
|
||||||
|
#[values(LidarrEvent::GetHostConfig, LidarrEvent::GetSecurityConfig)] event: LidarrEvent,
|
||||||
|
) {
|
||||||
|
assert_str_eq!(event.resource(), "/config/host");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_resource_command(#[values(LidarrEvent::UpdateAllArtists)] event: LidarrEvent) {
|
||||||
|
assert_str_eq!(event.resource(), "/command");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
|
||||||
|
#[case(LidarrEvent::GetDownloads(500), "/queue")]
|
||||||
|
#[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")]
|
||||||
|
#[case(LidarrEvent::GetQualityProfiles, "/qualityprofile")]
|
||||||
|
#[case(LidarrEvent::GetRootFolders, "/rootfolder")]
|
||||||
|
#[case(LidarrEvent::GetStatus, "/system/status")]
|
||||||
|
#[case(LidarrEvent::GetTags, "/tag")]
|
||||||
|
#[case(LidarrEvent::HealthCheck, "/health")]
|
||||||
|
fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) {
|
||||||
|
assert_str_eq!(event.resource(), expected_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_lidarr_event() {
|
||||||
|
assert_eq!(
|
||||||
|
NetworkEvent::Lidarr(LidarrEvent::HealthCheck),
|
||||||
|
NetworkEvent::from(LidarrEvent::HealthCheck)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_lidarr_healthcheck_event() {
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.build_for(LidarrEvent::HealthCheck)
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let _ = network.handle_lidarr_event(LidarrEvent::HealthCheck).await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_metadata_profiles_event() {
|
||||||
|
let metadata_profiles_json = json!([{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Standard"
|
||||||
|
}]);
|
||||||
|
let response: Vec<MetadataProfile> =
|
||||||
|
serde_json::from_value(metadata_profiles_json.clone()).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(metadata_profiles_json)
|
||||||
|
.build_for(LidarrEvent::GetMetadataProfiles)
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::GetMetadataProfiles)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else {
|
||||||
|
panic!("Expected MetadataProfiles");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(metadata_profiles, response);
|
||||||
|
assert_eq!(
|
||||||
|
app
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.metadata_profile_map
|
||||||
|
.get_by_left(&1),
|
||||||
|
Some(&"Standard".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_quality_profiles_event() {
|
||||||
|
let quality_profiles_json = json!([{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Lossless"
|
||||||
|
}]);
|
||||||
|
let response: Vec<QualityProfile> =
|
||||||
|
serde_json::from_value(quality_profiles_json.clone()).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(quality_profiles_json)
|
||||||
|
.build_for(LidarrEvent::GetQualityProfiles)
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::GetQualityProfiles)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else {
|
||||||
|
panic!("Expected QualityProfiles");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(quality_profiles, response);
|
||||||
|
assert_eq!(
|
||||||
|
app
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.quality_profile_map
|
||||||
|
.get_by_left(&1),
|
||||||
|
Some(&"Lossless".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_tags_event() {
|
||||||
|
let tags_json = json!([{
|
||||||
|
"id": 1,
|
||||||
|
"label": "usenet"
|
||||||
|
}]);
|
||||||
|
let response: Vec<Tag> = serde_json::from_value(tags_json.clone()).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(tags_json)
|
||||||
|
.build_for(LidarrEvent::GetTags)
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network.handle_lidarr_event(LidarrEvent::GetTags).await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::Tags(tags) = result.unwrap() else {
|
||||||
|
panic!("Expected Tags");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(tags, response);
|
||||||
|
assert_eq!(
|
||||||
|
app.lock().await.data.lidarr_data.tags_map.get_by_left(&1),
|
||||||
|
Some(&"usenet".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_add_lidarr_tag_event() {
|
||||||
|
let tag_json = json!({
|
||||||
|
"id": 1,
|
||||||
|
"label": "usenet"
|
||||||
|
});
|
||||||
|
let response: Tag = serde_json::from_value(tag_json.clone()).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::post()
|
||||||
|
.with_request_body(json!({ "label": "usenet" }))
|
||||||
|
.returns(tag_json)
|
||||||
|
.build_for(LidarrEvent::AddTag("usenet".to_owned()))
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::AddTag("usenet".to_owned()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::Tag(tag) = result.unwrap() else {
|
||||||
|
panic!("Expected Tag");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(tag, response);
|
||||||
|
assert_eq!(
|
||||||
|
app.lock().await.data.lidarr_data.tags_map.get_by_left(&1),
|
||||||
|
Some(&"usenet".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_delete_lidarr_tag_event() {
|
||||||
|
let (mock, app, _server) = MockServarrApi::delete()
|
||||||
|
.path("/1")
|
||||||
|
.build_for(LidarrEvent::DeleteTag(1))
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network.handle_lidarr_event(LidarrEvent::DeleteTag(1)).await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_extract_and_add_lidarr_tag_ids_vec() {
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let tags = " test,HI ,, usenet ";
|
||||||
|
{
|
||||||
|
let mut app = app_arc.lock().await;
|
||||||
|
app.data.lidarr_data.tags_map = BiMap::from_iter([
|
||||||
|
(1, "usenet".to_owned()),
|
||||||
|
(2, "test".to_owned()),
|
||||||
|
(3, "hi".to_owned()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
app_arc.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app_arc);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
network.extract_and_add_lidarr_tag_ids_vec(tags).await,
|
||||||
|
vec![2, 3, 1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_extract_and_add_lidarr_tag_ids_vec_add_missing_tags_first() {
|
||||||
|
let (mock, app, _server) = MockServarrApi::post()
|
||||||
|
.with_request_body(json!({ "label": "TESTING" }))
|
||||||
|
.returns(json!({ "id": 3, "label": "testing" }))
|
||||||
|
.build_for(LidarrEvent::GetTags)
|
||||||
|
.await;
|
||||||
|
let tags = "usenet, test, TESTING";
|
||||||
|
{
|
||||||
|
let mut app_guard = app.lock().await;
|
||||||
|
app_guard.data.lidarr_data.edit_artist_modal = Some(EditArtistModal {
|
||||||
|
tags: tags.into(),
|
||||||
|
..EditArtistModal::default()
|
||||||
|
});
|
||||||
|
app_guard.data.lidarr_data.tags_map =
|
||||||
|
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
|
||||||
|
}
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let tag_ids_vec = network.extract_and_add_lidarr_tag_ids_vec(tags).await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
assert_eq!(tag_ids_vec, vec![1, 2, 3]);
|
||||||
|
assert_eq!(
|
||||||
|
app.lock().await.data.lidarr_data.tags_map,
|
||||||
|
BiMap::from_iter([
|
||||||
|
(1, "usenet".to_owned()),
|
||||||
|
(2, "test".to_owned()),
|
||||||
|
(3, "testing".to_owned())
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use super::{NetworkEvent, NetworkResource};
|
||||||
|
use crate::models::lidarr_models::{
|
||||||
|
DeleteArtistParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
|
||||||
|
};
|
||||||
|
use crate::models::servarr_models::{QualityProfile, Tag};
|
||||||
|
use crate::network::{Network, RequestMethod};
|
||||||
|
|
||||||
|
mod downloads;
|
||||||
|
mod library;
|
||||||
|
mod root_folders;
|
||||||
|
mod system;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "lidarr_network_tests.rs"]
|
||||||
|
mod lidarr_network_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "lidarr_network_test_utils.rs"]
|
||||||
|
pub mod lidarr_network_test_utils;
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
|
pub enum LidarrEvent {
|
||||||
|
AddTag(String),
|
||||||
|
DeleteArtist(DeleteArtistParams),
|
||||||
|
DeleteTag(i64),
|
||||||
|
EditArtist(EditArtistParams),
|
||||||
|
GetArtistDetails(i64),
|
||||||
|
GetDiskSpace,
|
||||||
|
GetDownloads(u64),
|
||||||
|
GetHostConfig,
|
||||||
|
GetMetadataProfiles,
|
||||||
|
GetQualityProfiles,
|
||||||
|
GetRootFolders,
|
||||||
|
GetSecurityConfig,
|
||||||
|
GetStatus,
|
||||||
|
GetTags,
|
||||||
|
HealthCheck,
|
||||||
|
ListArtists,
|
||||||
|
SearchNewArtist(String),
|
||||||
|
ToggleArtistMonitoring(i64),
|
||||||
|
UpdateAllArtists,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkResource for LidarrEvent {
|
||||||
|
fn resource(&self) -> &'static str {
|
||||||
|
match &self {
|
||||||
|
LidarrEvent::AddTag(_) | LidarrEvent::DeleteTag(_) | LidarrEvent::GetTags => "/tag",
|
||||||
|
LidarrEvent::DeleteArtist(_)
|
||||||
|
| LidarrEvent::EditArtist(_)
|
||||||
|
| LidarrEvent::GetArtistDetails(_)
|
||||||
|
| LidarrEvent::ListArtists
|
||||||
|
| LidarrEvent::ToggleArtistMonitoring(_) => "/artist",
|
||||||
|
LidarrEvent::GetDiskSpace => "/diskspace",
|
||||||
|
LidarrEvent::GetDownloads(_) => "/queue",
|
||||||
|
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
|
||||||
|
LidarrEvent::UpdateAllArtists => "/command",
|
||||||
|
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
|
||||||
|
LidarrEvent::GetQualityProfiles => "/qualityprofile",
|
||||||
|
LidarrEvent::GetRootFolders => "/rootfolder",
|
||||||
|
LidarrEvent::GetStatus => "/system/status",
|
||||||
|
LidarrEvent::HealthCheck => "/health",
|
||||||
|
LidarrEvent::SearchNewArtist(_) => "/artist/lookup",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LidarrEvent> for NetworkEvent {
|
||||||
|
fn from(lidarr_event: LidarrEvent) -> Self {
|
||||||
|
NetworkEvent::Lidarr(lidarr_event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Network<'_, '_> {
|
||||||
|
pub async fn handle_lidarr_event(
|
||||||
|
&mut self,
|
||||||
|
lidarr_event: LidarrEvent,
|
||||||
|
) -> Result<LidarrSerdeable> {
|
||||||
|
match lidarr_event {
|
||||||
|
LidarrEvent::AddTag(tag) => self.add_lidarr_tag(tag).await.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::DeleteArtist(params) => {
|
||||||
|
self.delete_artist(params).await.map(LidarrSerdeable::from)
|
||||||
|
}
|
||||||
|
LidarrEvent::DeleteTag(tag_id) => self
|
||||||
|
.delete_lidarr_tag(tag_id)
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetArtistDetails(artist_id) => self
|
||||||
|
.get_artist_details(artist_id)
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetDownloads(count) => self
|
||||||
|
.get_lidarr_downloads(count)
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetHostConfig => self
|
||||||
|
.get_lidarr_host_config()
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetMetadataProfiles => self
|
||||||
|
.get_lidarr_metadata_profiles()
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetQualityProfiles => self
|
||||||
|
.get_lidarr_quality_profiles()
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetRootFolders => self
|
||||||
|
.get_lidarr_root_folders()
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetSecurityConfig => self
|
||||||
|
.get_lidarr_security_config()
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetStatus => self.get_lidarr_status().await.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::GetTags => self.get_lidarr_tags().await.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::HealthCheck => self
|
||||||
|
.get_lidarr_healthcheck()
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::ListArtists => self.list_artists().await.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::SearchNewArtist(query) => {
|
||||||
|
self.search_artist(query).await.map(LidarrSerdeable::from)
|
||||||
|
}
|
||||||
|
LidarrEvent::ToggleArtistMonitoring(artist_id) => self
|
||||||
|
.toggle_artist_monitoring(artist_id)
|
||||||
|
.await
|
||||||
|
.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from),
|
||||||
|
LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::network::lidarr_network) async fn get_lidarr_healthcheck(&mut self) -> Result<()> {
|
||||||
|
info!("Performing Lidarr health check");
|
||||||
|
let event = LidarrEvent::HealthCheck;
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_lidarr_metadata_profiles(&mut self) -> Result<Vec<MetadataProfile>> {
|
||||||
|
info!("Fetching Lidarr metadata profiles");
|
||||||
|
let event = LidarrEvent::GetMetadataProfiles;
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), Vec<MetadataProfile>>(request_props, |metadata_profiles, mut app| {
|
||||||
|
app.data.lidarr_data.metadata_profile_map = metadata_profiles
|
||||||
|
.into_iter()
|
||||||
|
.map(|profile| (profile.id, profile.name))
|
||||||
|
.collect();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_lidarr_quality_profiles(&mut self) -> Result<Vec<QualityProfile>> {
|
||||||
|
info!("Fetching Lidarr quality profiles");
|
||||||
|
let event = LidarrEvent::GetQualityProfiles;
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
|
||||||
|
app.data.lidarr_data.quality_profile_map = quality_profiles
|
||||||
|
.into_iter()
|
||||||
|
.map(|profile| (profile.id, profile.name))
|
||||||
|
.collect();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_lidarr_tags(&mut self) -> Result<Vec<Tag>> {
|
||||||
|
info!("Fetching Lidarr tags");
|
||||||
|
let event = LidarrEvent::GetTags;
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
|
||||||
|
app.data.lidarr_data.tags_map = tags_vec
|
||||||
|
.into_iter()
|
||||||
|
.map(|tag| (tag.id, tag.label))
|
||||||
|
.collect();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_lidarr_tag(&mut self, tag: String) -> Result<Tag> {
|
||||||
|
info!("Adding a new Lidarr tag");
|
||||||
|
let event = LidarrEvent::AddTag(String::new());
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
event,
|
||||||
|
RequestMethod::Post,
|
||||||
|
Some(serde_json::json!({ "label": tag })),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<serde_json::Value, Tag>(request_props, |tag, mut app| {
|
||||||
|
app.data.lidarr_data.tags_map.insert(tag.id, tag.label);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_lidarr_tag(&mut self, id: i64) -> Result<()> {
|
||||||
|
info!("Deleting Lidarr tag with ID: {id}");
|
||||||
|
let event = LidarrEvent::DeleteTag(id);
|
||||||
|
|
||||||
|
let request_props = self
|
||||||
|
.request_props_from(
|
||||||
|
event,
|
||||||
|
RequestMethod::Delete,
|
||||||
|
None::<()>,
|
||||||
|
Some(format!("/{id}")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self
|
||||||
|
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::network::lidarr_network) async fn extract_and_add_lidarr_tag_ids_vec(
|
||||||
|
&mut self,
|
||||||
|
edit_tags: &str,
|
||||||
|
) -> Vec<i64> {
|
||||||
|
let missing_tags_vec = {
|
||||||
|
let tags_map = &self.app.lock().await.data.lidarr_data.tags_map;
|
||||||
|
edit_tags
|
||||||
|
.split(',')
|
||||||
|
.filter(|&tag| {
|
||||||
|
!tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none()
|
||||||
|
})
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
for tag in missing_tags_vec {
|
||||||
|
self
|
||||||
|
.add_lidarr_tag(tag.trim().to_owned())
|
||||||
|
.await
|
||||||
|
.expect("Unable to add tag");
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = self.app.lock().await;
|
||||||
|
edit_tags
|
||||||
|
.split(',')
|
||||||
|
.filter(|tag| !tag.is_empty())
|
||||||
|
.map(|tag| {
|
||||||
|
*app
|
||||||
|
.data
|
||||||
|
.lidarr_data
|
||||||
|
.tags_map
|
||||||
|
.get_by_right(tag.to_lowercase().trim())
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::models::lidarr_models::LidarrSerdeable;
|
||||||
|
use crate::models::servarr_models::RootFolder;
|
||||||
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
|
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_root_folders_event() {
|
||||||
|
let root_folders_json = json!([{
|
||||||
|
"id": 1,
|
||||||
|
"path": "/music",
|
||||||
|
"accessible": true,
|
||||||
|
"freeSpace": 50000000000i64
|
||||||
|
}]);
|
||||||
|
let response: Vec<RootFolder> = serde_json::from_value(root_folders_json.clone()).unwrap();
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(root_folders_json)
|
||||||
|
.build_for(LidarrEvent::GetRootFolders)
|
||||||
|
.await;
|
||||||
|
app.lock().await.server_tabs.set_index(2);
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_lidarr_event(LidarrEvent::GetRootFolders)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else {
|
||||||
|
panic!("Expected RootFolders");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(root_folders, response);
|
||||||
|
assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user