Compare commits
24 Commits
tachyonfx
...
4abf705cb5
| 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 |
Generated
+1037
-397
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -42,7 +42,7 @@ strum = { version = "0.26.3", features = ["derive"] }
|
||||
strum_macros = "0.26.4"
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
tokio-util = "0.7.8"
|
||||
ratatui = { version = "0.29.0", features = [
|
||||
ratatui = { version = "0.30.0", features = [
|
||||
"all-widgets",
|
||||
"unstable-widget-ref",
|
||||
] }
|
||||
@@ -59,7 +59,7 @@ ctrlc = "3.4.5"
|
||||
colored = "3.0.0"
|
||||
async-trait = "0.1.83"
|
||||
dirs-next = "2.0.0"
|
||||
managarr-tree-widget = "0.24.0"
|
||||
managarr-tree-widget = "0.25.0"
|
||||
indicatif = "0.17.9"
|
||||
derive_setters = "0.1.6"
|
||||
deunicode = "1.6.0"
|
||||
|
||||
@@ -8,6 +8,7 @@ mod tests {
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||
@@ -35,6 +36,7 @@ mod tests {
|
||||
theme: None,
|
||||
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
||||
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
||||
lidarr: None,
|
||||
};
|
||||
let expected_tab_routes = vec![
|
||||
TabRoute {
|
||||
@@ -184,6 +186,7 @@ mod tests {
|
||||
..SonarrData::default()
|
||||
};
|
||||
let data = Data {
|
||||
lidarr_data: LidarrData::default(),
|
||||
radarr_data,
|
||||
sonarr_data,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
|
||||
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
|
||||
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
|
||||
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
|
||||
use crate::models::Route;
|
||||
@@ -21,6 +22,7 @@ impl ContextClueProvider for ServarrContextClueProvider {
|
||||
match app.get_current_route() {
|
||||
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
|
||||
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
|
||||
Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
+53
-1
@@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use veil::Redact;
|
||||
|
||||
use crate::cli::Command;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
@@ -25,6 +26,7 @@ mod app_tests;
|
||||
pub mod context_clues;
|
||||
pub mod key_binding;
|
||||
mod key_binding_tests;
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
@@ -96,6 +98,26 @@ impl App<'_> {
|
||||
server_tabs.extend(sonarr_tabs);
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = config.lidarr {
|
||||
let mut unnamed_idx = 0;
|
||||
let lidarr_tabs = lidarr_configs.into_iter().map(|lidarr_config| {
|
||||
let name = if let Some(name) = lidarr_config.name.clone() {
|
||||
name
|
||||
} else {
|
||||
unnamed_idx += 1;
|
||||
format!("Lidarr {unnamed_idx}")
|
||||
};
|
||||
|
||||
TabRoute {
|
||||
title: name,
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(lidarr_config),
|
||||
}
|
||||
});
|
||||
server_tabs.extend(lidarr_tabs);
|
||||
}
|
||||
|
||||
let weight_sorted_tabs = server_tabs
|
||||
.into_iter()
|
||||
.sorted_by(|tab1, tab2| {
|
||||
@@ -176,6 +198,7 @@ impl App<'_> {
|
||||
match self.get_current_route() {
|
||||
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
|
||||
Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
|
||||
Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -264,6 +287,12 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Lidarr".to_owned(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
@@ -272,6 +301,7 @@ impl App<'_> {
|
||||
pub fn test_default_fully_populated() -> Self {
|
||||
App {
|
||||
data: Data {
|
||||
lidarr_data: LidarrData::test_default_fully_populated(),
|
||||
radarr_data: RadarrData::test_default_fully_populated(),
|
||||
sonarr_data: SonarrData::test_default_fully_populated(),
|
||||
},
|
||||
@@ -288,6 +318,12 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Lidarr".to_owned(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
@@ -296,6 +332,7 @@ impl App<'_> {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Data<'a> {
|
||||
pub lidarr_data: LidarrData<'a>,
|
||||
pub radarr_data: RadarrData<'a>,
|
||||
pub sonarr_data: SonarrData<'a>,
|
||||
}
|
||||
@@ -303,13 +340,14 @@ pub struct Data<'a> {
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub theme: Option<String>,
|
||||
pub lidarr: Option<Vec<ServarrConfig>>,
|
||||
pub radarr: Option<Vec<ServarrConfig>>,
|
||||
pub sonarr: Option<Vec<ServarrConfig>>,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn validate(&self) {
|
||||
if self.radarr.is_none() && self.sonarr.is_none() {
|
||||
if self.lidarr.is_none() && self.radarr.is_none() && self.sonarr.is_none() {
|
||||
log_and_print_error(
|
||||
"No Servarr configuration provided in the specified configuration file".to_owned(),
|
||||
);
|
||||
@@ -323,6 +361,10 @@ impl AppConfig {
|
||||
if let Some(sonarr_configs) = &self.sonarr {
|
||||
sonarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = &self.lidarr {
|
||||
lidarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_config_present_for_cli(&self, command: &Command) {
|
||||
@@ -340,6 +382,10 @@ impl AppConfig {
|
||||
msg("Sonarr");
|
||||
process::exit(1);
|
||||
}
|
||||
Command::Lidarr(_) if self.lidarr.is_none() => {
|
||||
msg("Lidarr");
|
||||
process::exit(1);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -356,6 +402,12 @@ impl AppConfig {
|
||||
sonarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = self.lidarr.as_mut() {
|
||||
for lidarr_config in lidarr_configs {
|
||||
lidarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -455,7 +455,6 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
// This should panic because the route is not a Sonarr route
|
||||
SonarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,13 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_subcommand_delegates_to_lidarr() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completions_requires_argument() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||
@@ -174,4 +181,6 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
// TODO: Implement test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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 clap::{Subcommand, command};
|
||||
use clap_complete::Shell;
|
||||
use lidarr::{LidarrCliHandler, LidarrCommand};
|
||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
@@ -24,6 +26,9 @@ pub enum Command {
|
||||
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
|
||||
Sonarr(SonarrCommand),
|
||||
|
||||
#[command(subcommand, about = "Commands for manging your Lidarr instance")]
|
||||
Lidarr(LidarrCommand),
|
||||
|
||||
#[command(
|
||||
arg_required_else_help = true,
|
||||
about = "Generate shell completions for the Managarr CLI"
|
||||
@@ -61,6 +66,11 @@ pub(crate) async fn handle_command(
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Command::Lidarr(lidarr_command) => {
|
||||
LidarrCliHandler::with(app, lidarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ mod property_tests {
|
||||
|
||||
use crate::app::App;
|
||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::{Scrollable, Paginated};
|
||||
use crate::models::{Paginated, Scrollable};
|
||||
|
||||
proptest! {
|
||||
/// Property test: Table never panics on index selection
|
||||
#[test]
|
||||
fn test_table_index_selection_safety(
|
||||
list_size in list_size(),
|
||||
@@ -25,19 +24,15 @@ mod property_tests {
|
||||
|
||||
table.set_items(movies);
|
||||
|
||||
// Try to select an arbitrary index
|
||||
if index < list_size {
|
||||
table.select_index(Some(index));
|
||||
let selected = table.current_selection();
|
||||
prop_assert_eq!(selected.id, index as i64);
|
||||
} else {
|
||||
// Out of bounds selection should be safe
|
||||
table.select_index(Some(index));
|
||||
// Should not panic, selection stays valid
|
||||
}
|
||||
}
|
||||
|
||||
/// Property test: Table state remains consistent after scroll operations
|
||||
#[test]
|
||||
fn test_table_scroll_consistency(
|
||||
list_size in list_size(),
|
||||
@@ -53,42 +48,34 @@ mod property_tests {
|
||||
table.set_items(movies);
|
||||
let initial_id = table.current_selection().id;
|
||||
|
||||
// Scroll down multiple times
|
||||
for _ in 0..scroll_amount {
|
||||
table.scroll_down();
|
||||
}
|
||||
let after_down_id = table.current_selection().id;
|
||||
|
||||
// Position should increase (up to max)
|
||||
prop_assert!(after_down_id >= initial_id);
|
||||
prop_assert!(after_down_id < list_size as i64);
|
||||
|
||||
// Scroll back up
|
||||
for _ in 0..scroll_amount {
|
||||
table.scroll_up();
|
||||
}
|
||||
|
||||
// Should return to initial position (or 0 if we hit the top)
|
||||
prop_assert!(table.current_selection().id <= initial_id);
|
||||
}
|
||||
|
||||
/// Property test: Empty tables handle operations gracefully
|
||||
#[test]
|
||||
fn test_empty_table_safety(_scroll_ops in 0usize..50) {
|
||||
let table = StatefulTable::<Movie>::default();
|
||||
|
||||
// Empty table operations should be safe
|
||||
prop_assert!(table.is_empty());
|
||||
prop_assert!(table.items.is_empty());
|
||||
}
|
||||
|
||||
/// Property test: Navigation operations maintain consistency
|
||||
#[test]
|
||||
fn test_navigation_consistency(pushes in 1usize..20) {
|
||||
let mut app = App::test_default();
|
||||
let initial_route = app.get_current_route();
|
||||
|
||||
// Push multiple routes
|
||||
let routes = vec![
|
||||
ActiveRadarrBlock::Movies,
|
||||
ActiveRadarrBlock::Collections,
|
||||
@@ -101,34 +88,27 @@ mod property_tests {
|
||||
app.push_navigation_stack(route.into());
|
||||
}
|
||||
|
||||
// Current route should be the last pushed
|
||||
let last_pushed = routes[(pushes - 1) % routes.len()];
|
||||
prop_assert_eq!(app.get_current_route(), last_pushed.into());
|
||||
|
||||
// Pop all routes
|
||||
for _ in 0..pushes {
|
||||
app.pop_navigation_stack();
|
||||
}
|
||||
|
||||
// Should return to initial route
|
||||
prop_assert_eq!(app.get_current_route(), initial_route);
|
||||
}
|
||||
|
||||
/// Property test: String input handling is safe
|
||||
#[test]
|
||||
fn test_string_input_safety(input in text_input_string()) {
|
||||
// String operations should never panic
|
||||
let _lowercase = input.to_lowercase();
|
||||
let _uppercase = input.to_uppercase();
|
||||
let _trimmed = input.trim();
|
||||
let _len = input.len();
|
||||
let _chars: Vec<char> = input.chars().collect();
|
||||
|
||||
// All operations completed without panic
|
||||
prop_assert!(true);
|
||||
}
|
||||
|
||||
/// Property test: Table maintains data integrity after operations
|
||||
#[test]
|
||||
fn test_table_data_integrity(
|
||||
list_size in 1usize..100
|
||||
@@ -144,16 +124,13 @@ mod property_tests {
|
||||
table.set_items(movies.clone());
|
||||
let original_count = table.items.len();
|
||||
|
||||
// Count should remain the same after various operations
|
||||
prop_assert_eq!(table.items.len(), original_count);
|
||||
|
||||
// All original items should still be present
|
||||
for movie in &movies {
|
||||
prop_assert!(table.items.iter().any(|m| m.id == movie.id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Property test: Page up/down maintains bounds
|
||||
#[test]
|
||||
fn test_page_navigation_bounds(
|
||||
list_size in list_size(),
|
||||
@@ -168,7 +145,6 @@ mod property_tests {
|
||||
|
||||
table.set_items(movies);
|
||||
|
||||
// Perform page operations
|
||||
for i in 0..page_ops {
|
||||
if i % 2 == 0 {
|
||||
table.page_down();
|
||||
@@ -176,14 +152,12 @@ mod property_tests {
|
||||
table.page_up();
|
||||
}
|
||||
|
||||
// Should never exceed bounds
|
||||
let current = table.current_selection();
|
||||
prop_assert!(current.id >= 0);
|
||||
prop_assert!(current.id < list_size as i64);
|
||||
}
|
||||
}
|
||||
|
||||
/// Property test: Table filtering reduces or maintains size
|
||||
#[test]
|
||||
fn test_table_filter_size_invariant(
|
||||
list_size in list_size(),
|
||||
@@ -200,7 +174,6 @@ mod property_tests {
|
||||
table.set_items(movies.clone());
|
||||
let original_size = table.items.len();
|
||||
|
||||
// Apply filter
|
||||
if !filter_term.is_empty() {
|
||||
let filtered: Vec<Movie> = movies.into_iter()
|
||||
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
|
||||
@@ -208,10 +181,8 @@ mod property_tests {
|
||||
table.set_items(filtered);
|
||||
}
|
||||
|
||||
// Filtered size should be <= original
|
||||
prop_assert!(table.items.len() <= original_size);
|
||||
|
||||
// Selection should still be valid if table not empty
|
||||
if !table.items.is_empty() {
|
||||
let current = table.current_selection();
|
||||
prop_assert!(current.id >= 0);
|
||||
|
||||
@@ -21,6 +21,7 @@ mod tests {
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
@@ -60,11 +61,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)]
|
||||
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)]
|
||||
fn test_handle_change_tabs<T>(#[case] index: usize, #[case] left_block: T, #[case] right_block: T)
|
||||
where
|
||||
#[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)]
|
||||
#[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)]
|
||||
#[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)]
|
||||
fn test_handle_change_tabs<T, U>(
|
||||
#[case] index: usize,
|
||||
#[case] left_block: T,
|
||||
#[case] right_block: U,
|
||||
) where
|
||||
T: Into<Route> + Copy,
|
||||
U: Into<Route> + Copy,
|
||||
{
|
||||
let mut app = App::test_default();
|
||||
app.error = "Test".into();
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -75,7 +76,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandle
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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 sonarr_handlers::SonarrHandler;
|
||||
|
||||
@@ -15,6 +16,7 @@ use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{HorizontallyScrollableText, Route};
|
||||
|
||||
mod keybinding_handler;
|
||||
mod lidarr_handlers;
|
||||
mod radarr_handlers;
|
||||
mod sonarr_handlers;
|
||||
|
||||
@@ -125,6 +127,9 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
||||
Route::Sonarr(active_sonarr_block, context) => {
|
||||
SonarrHandler::new(key, app, active_sonarr_block, context).handle()
|
||||
}
|
||||
Route::Lidarr(active_lidarr_block, context) => {
|
||||
LidarrHandler::new(key, app, active_lidarr_block, context).handle()
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -187,6 +192,9 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
|
||||
Route::Sonarr(_, _) => {
|
||||
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
|
||||
}
|
||||
Route::Lidarr(_, _) => {
|
||||
app.data.lidarr_data.prompt_confirm = !app.data.lidarr_data.prompt_confirm
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::radarr_models::BlocklistItem;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
||||
EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "collection_details_handler_tests.rs"]
|
||||
@@ -148,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::radarr_models::EditCollectionParams;
|
||||
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -376,7 +376,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::radarr_models::Collection;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
mod collection_details_handler;
|
||||
@@ -179,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
|
||||
use crate::models::servarr_models::EditIndexerParams;
|
||||
@@ -527,7 +528,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::radarr_models::IndexerSettings;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||
@@ -293,7 +294,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestA
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
mod edit_indexer_handler;
|
||||
@@ -212,7 +212,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::models::servarr_data::radarr::modals::AddMovieModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::models::{BlockSelectionState, Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -558,7 +558,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::radarr_models::DeleteMovieParams;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
@@ -141,7 +142,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::radarr_models::EditMovieParams;
|
||||
use crate::models::servarr_data::radarr::modals::EditMovieModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS};
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -397,7 +397,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
|
||||
};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::models::{BlockSelectionState, Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -379,7 +379,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::handlers::radarr_handlers::indexers::IndexersHandler;
|
||||
use crate::handlers::radarr_handlers::library::LibraryHandler;
|
||||
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
|
||||
use crate::handlers::radarr_handlers::system::SystemHandler;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::{App, Key, matches_key};
|
||||
|
||||
@@ -112,7 +113,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS};
|
||||
use crate::models::servarr_models::AddRootFolderBody;
|
||||
use crate::models::{HorizontallyScrollableText, Route};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -231,7 +231,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||
use crate::matches_key;
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::{Route, Scrollable};
|
||||
|
||||
mod system_details_handler;
|
||||
|
||||
@@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::radarr_models::RadarrTaskName;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::sonarr_models::BlocklistItem;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
|
||||
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::sonarr_models::SonarrHistoryItem;
|
||||
@@ -121,7 +122,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
|
||||
use crate::models::servarr_models::EditIndexerParams;
|
||||
@@ -526,7 +527,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||
};
|
||||
@@ -202,7 +203,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ use crate::handlers::sonarr_handlers::indexers::test_all_indexers_handler::TestA
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
|
||||
mod edit_indexer_handler;
|
||||
@@ -211,7 +211,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, ActiveSonarrBlock,
|
||||
};
|
||||
use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult};
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::models::{BlockSelectionState, Route, Scrollable};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -625,7 +625,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::models::Route;
|
||||
use crate::models::sonarr_models::DeleteSeriesParams;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{
|
||||
@@ -143,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::servarr_data::sonarr::modals::EditSeriesModal;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS};
|
||||
use crate::models::sonarr_models::EditSeriesParams;
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -471,7 +471,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::library::season_details_handler::releases_
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS};
|
||||
use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
@@ -370,7 +371,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandle
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use crate::handlers::sonarr_handlers::library::episode_details_handler::EpisodeD
|
||||
use crate::handlers::sonarr_handlers::library::season_details_handler::SeasonDetailsHandler;
|
||||
use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::models::Route;
|
||||
|
||||
mod add_series_handler;
|
||||
mod delete_series_handler;
|
||||
@@ -245,7 +246,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::history::history_sorting_options;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::sonarr_models::{
|
||||
@@ -458,7 +459,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ use crate::handlers::sonarr_handlers::history::history_sorting_options;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -342,7 +342,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ use library::LibraryHandler;
|
||||
use root_folders::RootFoldersHandler;
|
||||
use system::SystemHandler;
|
||||
|
||||
use super::KeyEventHandler;
|
||||
use crate::models::Route;
|
||||
use crate::{
|
||||
app::App, event::Key, matches_key, models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
|
||||
};
|
||||
|
||||
use super::KeyEventHandler;
|
||||
|
||||
mod blocklist;
|
||||
mod downloads;
|
||||
mod history;
|
||||
@@ -115,7 +115,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use crate::event::Key;
|
||||
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS};
|
||||
use crate::models::servarr_models::AddRootFolderBody;
|
||||
use crate::models::{HorizontallyScrollableText, Route};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
|
||||
|
||||
@@ -229,7 +229,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<'
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::sonarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||
use crate::matches_key;
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::{Route, Scrollable};
|
||||
|
||||
mod system_details_handler;
|
||||
|
||||
@@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemHandler<'a, 'b
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS};
|
||||
use crate::models::sonarr_models::SonarrTaskName;
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
use crate::models::{Route, Scrollable};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod tests {
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::TableHandlingConfig;
|
||||
use crate::handlers::table_handler::handle_table;
|
||||
use crate::models::Route;
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::servarr_models::Language;
|
||||
@@ -98,7 +99,7 @@ mod tests {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
+21
-8
@@ -3,12 +3,15 @@
|
||||
extern crate assertables;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version};
|
||||
use clap::{
|
||||
Args, CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version,
|
||||
};
|
||||
use clap_complete::generate;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use log::{debug, error, warn};
|
||||
use network::NetworkTrait;
|
||||
use ratatui::Terminal;
|
||||
@@ -64,6 +67,13 @@ mod utils;
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
#[command(flatten)]
|
||||
global: GlobalOpts,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
#[command(next_help_heading = "Global Options")]
|
||||
struct GlobalOpts {
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
@@ -98,9 +108,12 @@ struct Cli {
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
help = "For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
||||
help = indoc!{"
|
||||
For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
||||
|
||||
This is useful when you have multiple instances of the same Servarr defined in your config file.
|
||||
By default, if left empty, the first configured Servarr instance listed in the config file will be used."
|
||||
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
|
||||
"}
|
||||
)]
|
||||
servarr_name: Option<String>,
|
||||
}
|
||||
@@ -114,13 +127,13 @@ async fn main() -> Result<()> {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
let args = Cli::parse();
|
||||
let mut config = if let Some(ref config_file) = args.config_file {
|
||||
let mut config = if let Some(ref config_file) = args.global.config_file {
|
||||
load_config(config_file.to_str().expect("Invalid config file specified"))?
|
||||
} else {
|
||||
confy::load("managarr", "config")?
|
||||
};
|
||||
let theme_name = config.theme.clone();
|
||||
let spinner_disabled = args.disable_spinner;
|
||||
let spinner_disabled = args.global.disable_spinner;
|
||||
debug!("Managarr loaded using config: {config:?}");
|
||||
config.validate();
|
||||
config.post_process_initialization();
|
||||
@@ -145,7 +158,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
match args.command {
|
||||
Some(command) => match command {
|
||||
Command::Radarr(_) | Command::Sonarr(_) => {
|
||||
Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) => {
|
||||
if spinner_disabled {
|
||||
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||
} else {
|
||||
@@ -165,8 +178,8 @@ async fn main() -> Result<()> {
|
||||
});
|
||||
start_ui(
|
||||
&app,
|
||||
&args.themes_file,
|
||||
args.theme.unwrap_or(theme_name.unwrap_or_default()),
|
||||
&args.global.themes_file,
|
||||
args.global.theme.unwrap_or(theme_name.unwrap_or_default()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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::context_clues::ContextClue;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use lidarr_models::LidarrSerdeable;
|
||||
use radarr_models::RadarrSerdeable;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
@@ -11,6 +13,7 @@ use serde_json::Number;
|
||||
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use sonarr_models::SonarrSerdeable;
|
||||
|
||||
pub mod lidarr_models;
|
||||
pub mod radarr_models;
|
||||
pub mod servarr_data;
|
||||
pub mod servarr_models;
|
||||
@@ -30,7 +33,7 @@ pub enum Route {
|
||||
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
|
||||
Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
|
||||
Readarr,
|
||||
Lidarr,
|
||||
Lidarr(ActiveLidarrBlock, Option<ActiveLidarrBlock>),
|
||||
Whisparr,
|
||||
Bazarr,
|
||||
Prowlarr,
|
||||
@@ -43,6 +46,7 @@ pub enum Route {
|
||||
pub enum Serdeable {
|
||||
Radarr(RadarrSerdeable),
|
||||
Sonarr(SonarrSerdeable),
|
||||
Lidarr(LidarrSerdeable),
|
||||
}
|
||||
|
||||
pub trait Scrollable {
|
||||
@@ -289,8 +293,7 @@ impl TabState {
|
||||
TabState { tabs, index: 0 }
|
||||
}
|
||||
|
||||
// Allowing this code for now since we'll eventually be implementing additional Servarr support, and we'll need it then
|
||||
#[allow(dead_code)]
|
||||
#[cfg(test)]
|
||||
pub fn set_index(&mut self, index: usize) -> &TabRoute {
|
||||
self.index = index;
|
||||
&self.tabs[self.index]
|
||||
|
||||
@@ -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;
|
||||
|
||||
pub mod lidarr;
|
||||
pub mod modals;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod radarr_data_tests {
|
||||
use chrono::{DateTime, Utc};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
|
||||
use crate::app::context_clues::{
|
||||
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_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,
|
||||
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::servarr_data::radarr::radarr_data::radarr_test_utils::utils;
|
||||
@@ -61,6 +62,45 @@ mod tests {
|
||||
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]
|
||||
fn test_radarr_data_defaults() {
|
||||
let radarr_data = RadarrData::default();
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::{
|
||||
mod modals_tests;
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub struct AddSeriesModal {
|
||||
pub root_folder_list: StatefulList<RootFolder>,
|
||||
pub monitor_list: StatefulList<SeriesMonitor>,
|
||||
@@ -130,6 +131,7 @@ impl From<&SonarrData<'_>> for EditIndexerModal {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub struct EditSeriesModal {
|
||||
pub series_type_list: StatefulList<SeriesType>,
|
||||
pub quality_profile_list: StatefulList<String>,
|
||||
@@ -260,6 +262,7 @@ impl Default for EpisodeDetailsModal {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub struct SeasonDetailsModal {
|
||||
pub episodes: StatefulTable<Episode>,
|
||||
pub episode_files: StatefulTable<EpisodeFile>,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#[cfg(test)]
|
||||
mod 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::models::sonarr_models::{Season, SonarrHistoryItem};
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
@@ -22,6 +19,10 @@ mod tests {
|
||||
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]
|
||||
fn test_from_active_sonarr_block_to_route() {
|
||||
@@ -77,44 +78,105 @@ mod tests {
|
||||
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]
|
||||
fn test_sonarr_data_defaults() {
|
||||
let sonarr_data = SonarrData::default();
|
||||
|
||||
assert!(!sonarr_data.add_list_exclusion);
|
||||
assert!(sonarr_data.add_searched_series.is_none());
|
||||
assert!(sonarr_data.add_series_search.is_none());
|
||||
assert!(sonarr_data.add_series_modal.is_none());
|
||||
assert!(sonarr_data.blocklist.is_empty());
|
||||
assert_none!(sonarr_data.add_searched_series);
|
||||
assert_none!(sonarr_data.add_series_search);
|
||||
assert_none!(sonarr_data.add_series_modal);
|
||||
assert_is_empty!(sonarr_data.blocklist);
|
||||
assert!(!sonarr_data.delete_series_files);
|
||||
assert!(sonarr_data.downloads.is_empty());
|
||||
assert!(sonarr_data.disk_space_vec.is_empty());
|
||||
assert!(sonarr_data.edit_indexer_modal.is_none());
|
||||
assert!(sonarr_data.edit_root_folder.is_none());
|
||||
assert!(sonarr_data.edit_series_modal.is_none());
|
||||
assert!(sonarr_data.history.is_empty());
|
||||
assert!(sonarr_data.indexers.is_empty());
|
||||
assert!(sonarr_data.indexer_settings.is_none());
|
||||
assert!(sonarr_data.indexer_test_errors.is_none());
|
||||
assert!(sonarr_data.indexer_test_all_results.is_none());
|
||||
assert!(sonarr_data.language_profiles_map.is_empty());
|
||||
assert!(sonarr_data.logs.is_empty());
|
||||
assert!(sonarr_data.log_details.is_empty());
|
||||
assert_is_empty!(sonarr_data.downloads);
|
||||
assert_is_empty!(sonarr_data.disk_space_vec);
|
||||
assert_none!(sonarr_data.edit_indexer_modal);
|
||||
assert_none!(sonarr_data.edit_root_folder);
|
||||
assert_none!(sonarr_data.edit_series_modal);
|
||||
assert_is_empty!(sonarr_data.history);
|
||||
assert_is_empty!(sonarr_data.indexers);
|
||||
assert_none!(sonarr_data.indexer_settings);
|
||||
assert_none!(sonarr_data.indexer_test_errors);
|
||||
assert_none!(sonarr_data.indexer_test_all_results);
|
||||
assert_is_empty!(sonarr_data.language_profiles_map);
|
||||
assert_is_empty!(sonarr_data.logs);
|
||||
assert_is_empty!(sonarr_data.log_details);
|
||||
assert!(!sonarr_data.prompt_confirm);
|
||||
assert!(sonarr_data.prompt_confirm_action.is_none());
|
||||
assert!(sonarr_data.quality_profile_map.is_empty());
|
||||
assert!(sonarr_data.queued_events.is_empty());
|
||||
assert!(sonarr_data.root_folders.is_empty());
|
||||
assert!(sonarr_data.seasons.is_empty());
|
||||
assert!(sonarr_data.season_details_modal.is_none());
|
||||
assert_none!(sonarr_data.prompt_confirm_action);
|
||||
assert_is_empty!(sonarr_data.quality_profile_map);
|
||||
assert_is_empty!(sonarr_data.queued_events);
|
||||
assert_is_empty!(sonarr_data.root_folders);
|
||||
assert_is_empty!(sonarr_data.seasons);
|
||||
assert_none!(sonarr_data.season_details_modal);
|
||||
assert_eq!(sonarr_data.selected_block, BlockSelectionState::default());
|
||||
assert!(sonarr_data.series.is_empty());
|
||||
assert!(sonarr_data.series_history.is_none());
|
||||
assert_is_empty!(sonarr_data.series);
|
||||
assert_none!(sonarr_data.series_history);
|
||||
assert_eq!(sonarr_data.start_time, <DateTime<Utc>>::default());
|
||||
assert!(sonarr_data.tags_map.is_empty());
|
||||
assert!(sonarr_data.tasks.is_empty());
|
||||
assert!(sonarr_data.updates.is_empty());
|
||||
assert!(sonarr_data.version.is_empty());
|
||||
assert_is_empty!(sonarr_data.tags_map);
|
||||
assert_is_empty!(sonarr_data.tasks);
|
||||
assert_is_empty!(sonarr_data.updates);
|
||||
assert_is_empty!(sonarr_data.version);
|
||||
|
||||
assert_eq!(sonarr_data.main_tabs.tabs.len(), 7);
|
||||
|
||||
@@ -123,84 +185,77 @@ mod tests {
|
||||
sonarr_data.main_tabs.tabs[0].route,
|
||||
ActiveSonarrBlock::Series.into()
|
||||
);
|
||||
assert!(sonarr_data.main_tabs.tabs[0].contextual_help.is_some());
|
||||
assert_eq!(
|
||||
sonarr_data.main_tabs.tabs[0].contextual_help.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.main_tabs.tabs[0].contextual_help,
|
||||
&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_eq!(
|
||||
sonarr_data.main_tabs.tabs[1].route,
|
||||
ActiveSonarrBlock::Downloads.into()
|
||||
);
|
||||
assert!(sonarr_data.main_tabs.tabs[1].contextual_help.is_some());
|
||||
assert_eq!(
|
||||
sonarr_data.main_tabs.tabs[1].contextual_help.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.main_tabs.tabs[1].contextual_help,
|
||||
&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_eq!(
|
||||
sonarr_data.main_tabs.tabs[2].route,
|
||||
ActiveSonarrBlock::Blocklist.into()
|
||||
);
|
||||
assert!(sonarr_data.main_tabs.tabs[2].contextual_help.is_some());
|
||||
assert_eq!(
|
||||
sonarr_data.main_tabs.tabs[2].contextual_help.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.main_tabs.tabs[2].contextual_help,
|
||||
&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_eq!(
|
||||
sonarr_data.main_tabs.tabs[3].route,
|
||||
ActiveSonarrBlock::History.into()
|
||||
);
|
||||
assert!(sonarr_data.main_tabs.tabs[3].contextual_help.is_some());
|
||||
assert_eq!(
|
||||
sonarr_data.main_tabs.tabs[3].contextual_help.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.main_tabs.tabs[3].contextual_help,
|
||||
&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_eq!(
|
||||
sonarr_data.main_tabs.tabs[4].route,
|
||||
ActiveSonarrBlock::RootFolders.into()
|
||||
);
|
||||
assert!(sonarr_data.main_tabs.tabs[4].contextual_help.is_some());
|
||||
assert_eq!(
|
||||
sonarr_data.main_tabs.tabs[4].contextual_help.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.main_tabs.tabs[4].contextual_help,
|
||||
&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_eq!(
|
||||
sonarr_data.main_tabs.tabs[5].route,
|
||||
ActiveSonarrBlock::Indexers.into()
|
||||
);
|
||||
assert!(sonarr_data.main_tabs.tabs[5].contextual_help.is_some());
|
||||
assert_eq!(
|
||||
sonarr_data.main_tabs.tabs[5].contextual_help.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.main_tabs.tabs[5].contextual_help,
|
||||
&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_eq!(
|
||||
sonarr_data.main_tabs.tabs[6].route,
|
||||
ActiveSonarrBlock::System.into()
|
||||
);
|
||||
assert!(sonarr_data.main_tabs.tabs[6].contextual_help.is_some());
|
||||
assert_eq!(
|
||||
sonarr_data.main_tabs.tabs[6].contextual_help.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.main_tabs.tabs[6].contextual_help,
|
||||
&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);
|
||||
|
||||
@@ -209,36 +264,22 @@ mod tests {
|
||||
sonarr_data.series_info_tabs.tabs[0].route,
|
||||
ActiveSonarrBlock::SeriesDetails.into()
|
||||
);
|
||||
assert!(
|
||||
sonarr_data.series_info_tabs.tabs[0]
|
||||
.contextual_help
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
sonarr_data.series_info_tabs.tabs[0]
|
||||
.contextual_help
|
||||
.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.series_info_tabs.tabs[0].contextual_help,
|
||||
&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_eq!(
|
||||
sonarr_data.series_info_tabs.tabs[1].route,
|
||||
ActiveSonarrBlock::SeriesHistory.into()
|
||||
);
|
||||
assert!(
|
||||
sonarr_data.series_info_tabs.tabs[1]
|
||||
.contextual_help
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
sonarr_data.series_info_tabs.tabs[1]
|
||||
.contextual_help
|
||||
.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&sonarr_data.series_info_tabs.tabs[1].contextual_help,
|
||||
&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>) {
|
||||
let items_len = filtered_items.len();
|
||||
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();
|
||||
filtered_state.select(Some(0));
|
||||
filtered_state.select(Some(preserved_selection));
|
||||
self.filtered_state = Some(filtered_state);
|
||||
}
|
||||
|
||||
|
||||
@@ -390,6 +390,47 @@ mod tests {
|
||||
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]
|
||||
fn test_stateful_table_current_selection() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_root_folders_network_tests.rs"]
|
||||
mod lidarr_root_folders_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders(
|
||||
&mut self,
|
||||
) -> Result<Vec<RootFolder>> {
|
||||
info!("Fetching Lidarr root folders");
|
||||
let event = LidarrEvent::GetRootFolders;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
|
||||
app.data.lidarr_data.root_folders.set_items(root_folders);
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user