Compare commits

...

59 Commits

Author SHA1 Message Date
eff1a901eb feat: CLI and TUI support for track history and track details in Lidarr 2026-01-19 14:50:20 -07:00
7add62b245 fix: Sonarr network wasn't checking for the user to be using the sorting block when populating season details 2026-01-19 14:50:01 -07:00
5fa9b08347 fix: Sonarr CLI was not properly filtering out episode and season releases when manually searching for releases 2026-01-19 14:49:17 -07:00
7bb5f83a56 feat: Lidarr UI support for album details popup 2026-01-16 23:21:25 -07:00
caf4ad1e64 feat: Implemented TUI handler support for the Album Details popup in Lidarr 2026-01-16 17:16:44 -07:00
bc6ecc39f4 feat: Bulk added CLI support for tracks and album functionalities in Lidarr 2026-01-16 14:38:08 -07:00
5e70d70758 test: Wrote tests for the Lidarr manual artist search key handler 2026-01-16 11:00:38 -07:00
1329589bd6 feat: Implemented the manual artist discography search tab in Lidarr's artist details UI 2026-01-15 14:36:09 -07:00
c6dc8f6090 feat: Lidarr CLI support for downloading a release 2026-01-15 12:57:54 -07:00
0ee275d58f fix: Sonarr manual search TUI and CLI incorrectly displaying the same unfiltered results for both season and episode searches 2026-01-15 12:43:16 -07:00
8dfa664a06 feat: CLI support for searching for discography releases in Lidarr 2026-01-15 11:39:34 -07:00
d7f0dd5950 feat: Added TUI and CLI support for viewing Artist history in Lidarr 2026-01-14 16:09:37 -07:00
8b9467bd39 feat: Full Lidarr system support for both the CLI and TUI 2026-01-14 14:50:33 -07:00
c74d5936d2 feat: Full CLI and TUI support for the Lidarr Indexers tab 2026-01-14 13:30:51 -07:00
8abcf44866 feat: Full support for adding a root folder in Lidarr from both the CLI and TUI 2026-01-14 09:06:27 -07:00
d2217509f2 feat: naive lidarr root folder tab implementation. Needs improved add logic 2026-01-13 14:33:12 -07:00
c68cd75015 feat: Downloads tab support in Lidarr 2026-01-13 13:40:18 -07:00
e1a25bfaf2 refactor: Improved and simplified the implementation of history details for both Sonarr and Lidarr 2026-01-13 12:36:21 -07:00
ad9e2b3671 feat: Created a History tab in the Radarr UI and created a list history command and mark-history-item-as-failed command for Radarr 2026-01-13 12:35:54 -07:00
0172253d20 fix: Slowed down the automatic text scrolling in tables so the text is readable 2026-01-12 15:28:41 -07:00
47fdee190a fix: Expanded the history item details size so that it can include all the available information for a given item; was previously being cut off on some screens 2026-01-12 15:28:03 -07:00
68b08d1cd7 feat: Implemented the Lidarr History tab and CLI support 2026-01-12 14:43:05 -07:00
f31810e48a feat: TUI support for deleting a Lidarr album from the artist details popup 2026-01-09 17:21:10 -07:00
09bee7473f feat: CLI support for deleting an album from Lidarr 2026-01-09 16:33:32 -07:00
b2814371f0 feat: Completed support for viewing Lidarr artist details 2026-01-09 16:22:03 -07:00
269057867f fix: Bug in submitting the update series prompt in the series details UI in Sonarr 2026-01-09 14:01:46 -07:00
450fdd7106 test: Added tests for the Lidarr context clues for the add artist popup 2026-01-08 15:20:17 -07:00
c624d1b9e4 feat: Full CLI and TUI support for adding an artist to Lidarr 2026-01-08 15:16:01 -07:00
e94f78dc7b refactor: Let serde serialize Add Series and Add Movie enums instead of calling to_string up front 2026-01-08 10:52:30 -07:00
b1a6db21f1 fix: Don't include Lidarr artist disambiguation in Edit popup title when it is empty 2026-01-08 10:09:15 -07:00
ca208ff5e4 fix: Refactored how quality profiles, language profiles, and metadata profiles are populated for each servarr so they sort using the ID to mimic the web UI better 2026-01-08 10:05:15 -07:00
1a43d1ec7c feat: Include the Lidarr artist disambiguation in the title of the Edit Artist popup 2026-01-08 09:28:56 -07:00
4abf705cb5 fix: Added the correct keybinding context to the Lidarr edit artist popup 2026-01-08 09:28:12 -07:00
cf98b10d77 style: Modified the LineGauge style so its more readable in the UI 2026-01-07 17:19:32 -07:00
f0ed71b436 build: Upgraded to Ratatui v0.30.0 and fixed a new security vulnerability [#13] 2026-01-07 17:15:54 -07:00
243de47cae feat: Initial Lidarr support for searching for new artists 2026-01-07 15:53:18 -07:00
d3947d9e15 fix: Improved fault tolerance for search result tables and test all indexer results tables 2026-01-07 14:58:32 -07:00
64d8c65831 fix: Prevented additional empty slice errors in indexer tables 2026-01-07 14:09:12 -07:00
60c4cf1098 fix: Fixed a bug in all Servarr implementations to not try to get the current selection of a search table when an error is returned from the API 2026-01-07 14:00:02 -07:00
9cc3ccb419 feat: Lidarr CLI commands to list quality profiles and metadata profiles 2026-01-07 13:15:50 -07:00
45c61369c8 feat: Improved CLI readability by creating a separate Global Options section for global flags 2026-01-07 13:08:23 -07:00
a8609e08c5 feat: CLI support for deleting a tag in Lidarr 2026-01-07 12:50:23 -07:00
a18b047f4f feat: Lidarr CLI support for listing and adding tags 2026-01-07 12:20:39 -07:00
b1afdaf541 feat: Added CLI and TUI support for editing Lidarr artists 2026-01-07 12:01:03 -07:00
3c1634d1e3 testing 2026-01-07 10:45:49 -07:00
9b4eda6a9d feat: Support for updating all Lidarr artists in both the CLI and TUI 2026-01-06 12:47:10 -07:00
96308afeee feat: Added Lidarr CLI support for fetching the host config and the security config 2026-01-06 11:00:19 -07:00
4e13d5d34d style: Applied formatting for the lidarr_command_tests 2026-01-06 10:25:05 -07:00
b4a99d1665 feat: Created Lidarr commands: 'get artist-details' and 'get system-status' 2026-01-06 10:24:51 -07:00
a012f6ecd5 feat: Fetch the artist members as part of the artist details query 2026-01-06 10:10:28 -07:00
5afee1998b feat: Support for toggling the monitoring of a given artist via the CLI and TUI 2026-01-06 09:40:16 -07:00
059fa48bd9 style: Applied uniform formatting across all new Lidarr files 2026-01-05 15:46:16 -07:00
6771a0ab38 feat: Full support for deleting an artist via CLI and TUI 2026-01-05 15:44:51 -07:00
bc3aeefa6e feat: TUI support for Lidarr library 2026-01-05 13:10:30 -07:00
e61537942b test: Implemented tests for the Lidarr list artists command 2026-01-05 11:28:35 -07:00
5d09b2402c feat: CLI support for listing artists 2026-01-05 10:58:48 -07:00
368f7505ff feat: Improved UI speed and responsiveness
Check / stable / fmt (push) Has been cancelled
Check / beta / clippy (push) Has been cancelled
Check / stable / clippy (push) Has been cancelled
Check / nightly / doc (push) Has been cancelled
Check / 1.89.0 / check (push) Has been cancelled
Test Suite / ubuntu / beta (push) Has been cancelled
Test Suite / ubuntu / stable (push) Has been cancelled
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
Test Suite / ubuntu / stable / coverage (push) Has been cancelled
2025-12-19 13:41:14 -07:00
6a9fd0999c build: Added additional compile-time optimizations for speed improvements and a smaller binary 2025-12-17 16:45:51 -07:00
Alex Clarke
d8ac94d067 Merge pull request #51 from Dark-Alex-17/test-refactoring
Refactored all tests and wrote snapshot tests to finally cover the UI module
2025-12-16 16:41:04 -07:00
527 changed files with 60563 additions and 2304 deletions
Generated
+1037 -397
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -42,7 +42,7 @@ strum = { version = "0.26.3", features = ["derive"] }
strum_macros = "0.26.4" strum_macros = "0.26.4"
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.44.2", features = ["full"] }
tokio-util = "0.7.8" tokio-util = "0.7.8"
ratatui = { version = "0.29.0", features = [ ratatui = { version = "0.30.0", features = [
"all-widgets", "all-widgets",
"unstable-widget-ref", "unstable-widget-ref",
] } ] }
@@ -59,7 +59,7 @@ ctrlc = "3.4.5"
colored = "3.0.0" colored = "3.0.0"
async-trait = "0.1.83" async-trait = "0.1.83"
dirs-next = "2.0.0" dirs-next = "2.0.0"
managarr-tree-widget = "0.24.0" managarr-tree-widget = "0.25.0"
indicatif = "0.17.9" indicatif = "0.17.9"
derive_setters = "0.1.6" derive_setters = "0.1.6"
deunicode = "1.6.0" deunicode = "1.6.0"
@@ -91,4 +91,6 @@ name = "managarr"
[profile.release] [profile.release]
lto = true lto = true
codegen-units = 1 codegen-units = 3
opt-level = "s"
strip = true
+27 -2
View File
@@ -8,6 +8,7 @@ mod tests {
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars}; use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::{HorizontallyScrollableText, TabRoute}; use crate::models::{HorizontallyScrollableText, TabRoute};
@@ -35,6 +36,7 @@ mod tests {
theme: None, theme: None,
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]), radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]), sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
lidarr: None,
}; };
let expected_tab_routes = vec![ let expected_tab_routes = vec![
TabRoute { TabRoute {
@@ -78,8 +80,9 @@ mod tests {
assert_eq!(app.server_tabs.index, 0); assert_eq!(app.server_tabs.index, 0);
assert_eq!(app.server_tabs.tabs, expected_tab_routes); assert_eq!(app.server_tabs.tabs, expected_tab_routes);
assert_eq!(app.tick_until_poll, 400); assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.ticks_until_scroll, 4); assert_eq!(app.ticks_until_scroll, 64);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
assert_eq!(app.ui_scroll_tick_count, 0);
assert!(!app.is_loading); assert!(!app.is_loading);
assert!(!app.is_routing); assert!(!app.is_routing);
assert!(!app.should_refresh); assert!(!app.should_refresh);
@@ -98,7 +101,7 @@ mod tests {
assert_eq!(app.error, HorizontallyScrollableText::default()); assert_eq!(app.error, HorizontallyScrollableText::default());
assert_eq!(app.server_tabs.index, 0); assert_eq!(app.server_tabs.index, 0);
assert_eq!(app.tick_until_poll, 400); assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.ticks_until_scroll, 4); assert_eq!(app.ticks_until_scroll, 64);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
assert!(!app.is_loading); assert!(!app.is_loading);
assert!(!app.is_routing); assert!(!app.is_routing);
@@ -183,6 +186,7 @@ mod tests {
..SonarrData::default() ..SonarrData::default()
}; };
let data = Data { let data = Data {
lidarr_data: LidarrData::default(),
radarr_data, radarr_data,
sonarr_data, sonarr_data,
}; };
@@ -240,6 +244,27 @@ mod tests {
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
} }
#[test]
fn test_on_ui_scroll_tick() {
let mut app = App {
ticks_until_scroll: 1,
..App::default()
};
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
app.on_ui_scroll_tick();
assert_eq!(app.ui_scroll_tick_count, 1);
assert_eq!(app.tick_count, 0);
app.on_ui_scroll_tick();
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
}
#[tokio::test] #[tokio::test]
async fn test_on_tick_first_render() { async fn test_on_tick_first_render() {
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500); let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
+19
View File
@@ -1,5 +1,6 @@
use crate::app::App; use crate::app::App;
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding}; use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider; use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider; use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
use crate::models::Route; use crate::models::Route;
@@ -21,6 +22,7 @@ impl ContextClueProvider for ServarrContextClueProvider {
match app.get_current_route() { match app.get_current_route() {
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app), Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app), Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app),
_ => None, _ => None,
} }
} }
@@ -100,6 +102,18 @@ pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
), ),
]; ];
pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [ pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
(DEFAULT_KEYBINDINGS.tasks, "open tasks"), (DEFAULT_KEYBINDINGS.tasks, "open tasks"),
(DEFAULT_KEYBINDINGS.events, "open events"), (DEFAULT_KEYBINDINGS.events, "open events"),
@@ -110,3 +124,8 @@ pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
), ),
]; ];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
+53 -10
View File
@@ -2,9 +2,9 @@
mod test { mod test {
use crate::app::context_clues::{ use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
ServarrContextClueProvider, SYSTEM_TASKS_CONTEXT_CLUES, ServarrContextClueProvider,
}; };
use crate::app::{App, key_binding::DEFAULT_KEYBINDINGS}; use crate::app::{App, key_binding::DEFAULT_KEYBINDINGS};
use crate::models::servarr_data::ActiveKeybindingBlock; use crate::models::servarr_data::ActiveKeybindingBlock;
@@ -204,6 +204,40 @@ mod test {
assert_none!(indexers_context_clues_iter.next()); assert_none!(indexers_context_clues_iter.next());
} }
#[test]
fn test_history_context_clues() {
let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_none!(history_context_clues_iter.next());
}
#[test] #[test]
fn test_system_context_clues() { fn test_system_context_clues() {
let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter(); let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter();
@@ -234,6 +268,21 @@ mod test {
assert_none!(system_context_clues_iter.next()); assert_none!(system_context_clues_iter.next());
} }
#[test]
fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
system_tasks_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "start task")
);
assert_some_eq_x!(
system_tasks_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(system_tasks_context_clues_iter.next());
}
#[test] #[test]
fn test_servarr_context_clue_provider_delegates_to_radarr_provider() { fn test_servarr_context_clue_provider_delegates_to_radarr_provider() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -241,10 +290,7 @@ mod test {
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!( assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
context_clues,
&crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
);
} }
#[test] #[test]
@@ -254,10 +300,7 @@ mod test {
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!( assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
context_clues,
&crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
);
} }
#[test] #[test]
+225
View File
@@ -0,0 +1,225 @@
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS,
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS,
TRACK_DETAILS_BLOCKS,
};
#[cfg(test)]
#[path = "lidarr_context_clues_tests.rs"]
mod lidarr_context_clues_tests;
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 10] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.update, "update all"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.delete, "delete album"),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
"toggle album monitoring",
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static ARTIST_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_ARTIST_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static ALBUM_DETAILS_CONTEXT_CLUES: [ContextClue; 6] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
(DEFAULT_KEYBINDINGS.submit, "track details"),
(DEFAULT_KEYBINDINGS.delete, "delete track"),
];
pub static ALBUM_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_ALBUM_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static TRACK_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static TRACK_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub(in crate::app) struct LidarrContextClueProvider;
impl ContextClueProvider for LidarrContextClueProvider {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() else {
panic!("LidarrContextClueProvider::get_context_clues called with non-Lidarr route");
};
match active_lidarr_block {
_ if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
.data
.lidarr_data
.artist_info_tabs
.get_active_route_contextual_help(),
_ if ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("album_details_modal is empty")
.album_details_tabs
.get_active_route_contextual_help(),
_ if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("album_details_modal is empty")
.track_details_modal
.as_ref()
.expect("track_details_modal is empty")
.track_details_tabs
.get_active_route_contextual_help(),
ActiveLidarrBlock::AddArtistSearchInput
| ActiveLidarrBlock::AddArtistEmptySearchResults
| ActiveLidarrBlock::TestAllIndexers
| ActiveLidarrBlock::SystemLogs
| ActiveLidarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
_ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block)
|| EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block)
|| INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block)
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveLidarrBlock::AddArtistPrompt
| ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder
| ActiveLidarrBlock::AddArtistTagsInput
| ActiveLidarrBlock::AddArtistAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
_ if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
Some(&ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES)
}
ActiveLidarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
_ => app
.data
.lidarr_data
.main_tabs
.get_active_route_contextual_help(),
}
}
}
@@ -0,0 +1,589 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::lidarr::lidarr_context_clues::{
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ALBUM_DETAILS_CONTEXT_CLUES,
ALBUM_HISTORY_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES,
ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES, TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS,
INDEXER_SETTINGS_BLOCKS, LidarrData,
};
use crate::models::servarr_data::lidarr::modals::{AlbumDetailsModal, TrackDetailsModal};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use rstest::rstest;
#[test]
fn test_artists_context_clues() {
let mut artists_context_clues_iter = ARTISTS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, "update all")
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_none!(artists_context_clues_iter.next());
}
#[test]
fn test_artist_details_context_clues() {
let mut artist_details_context_clues_iter = ARTIST_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete album")
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
"toggle album monitoring",
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(artist_details_context_clues_iter.next());
}
#[test]
fn test_add_artist_search_results_context_clues() {
let mut add_artist_search_results_context_clues_iter =
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
add_artist_search_results_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
add_artist_search_results_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "edit search")
);
assert_none!(add_artist_search_results_context_clues_iter.next());
}
#[test]
#[should_panic(
expected = "LidarrContextClueProvider::get_context_clues called with non-Lidarr route"
)]
fn test_lidarr_context_clue_provider_get_context_clues_non_lidarr_route() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::default().into());
LidarrContextClueProvider::get_context_clues(&mut app);
}
#[test]
fn test_artist_history_context_clues() {
let mut artist_history_context_clues_iter = ARTIST_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(artist_history_context_clues_iter.next());
}
#[test]
fn test_manual_artist_search_context_clues() {
let mut manual_artist_search_context_clues_iter = MANUAL_ARTIST_SEARCH_CONTEXT_CLUES.iter();
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(manual_artist_search_context_clues_iter.next());
}
#[test]
fn test_album_details_context_clues() {
let mut album_details_context_clues_iter = ALBUM_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "track details")
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete track")
);
assert_none!(album_details_context_clues_iter.next());
}
#[test]
fn test_album_history_context_clues() {
let mut album_history_context_clues_iter = ALBUM_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(album_history_context_clues_iter.next());
}
#[test]
fn test_manual_album_search_context_clues() {
let mut manual_album_search_context_clues_iter = MANUAL_ALBUM_SEARCH_CONTEXT_CLUES.iter();
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(manual_album_search_context_clues_iter.next());
}
#[test]
fn test_track_details_context_clues() {
let mut track_details_context_clues_iter = TRACK_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
track_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
track_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(track_details_context_clues_iter.next());
}
#[test]
fn test_track_history_context_clues() {
let mut track_history_context_clues_iter = TRACK_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
track_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(track_history_context_clues_iter.next());
}
#[rstest]
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::ArtistHistory, &ARTIST_HISTORY_CONTEXT_CLUES)]
#[case(2, ActiveLidarrBlock::ManualArtistSearch, &MANUAL_ARTIST_SEARCH_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_artist_info_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.lidarr_data = LidarrData::default();
app.data.lidarr_data.artist_info_tabs.set_index(index);
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[rstest]
#[case(0, ActiveLidarrBlock::AlbumDetails, &ALBUM_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::AlbumHistory, &ALBUM_HISTORY_CONTEXT_CLUES)]
#[case(2, ActiveLidarrBlock::ManualAlbumSearch, &MANUAL_ALBUM_SEARCH_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_album_details_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
let mut album_details_modal = AlbumDetailsModal::default();
album_details_modal.album_details_tabs.set_index(index);
let lidarr_data = LidarrData {
album_details_modal: Some(album_details_modal),
..LidarrData::default()
};
app.data.lidarr_data = lidarr_data;
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[rstest]
#[case(0, ActiveLidarrBlock::TrackDetails, &TRACK_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::TrackHistory, &TRACK_HISTORY_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_track_details_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
let mut track_details_modal = TrackDetailsModal::default();
track_details_modal.track_details_tabs.set_index(index);
let album_details_modal = AlbumDetailsModal {
track_details_modal: Some(track_details_modal),
..AlbumDetailsModal::default()
};
let lidarr_data = LidarrData {
album_details_modal: Some(album_details_modal),
..LidarrData::default()
};
app.data.lidarr_data = lidarr_data;
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[test]
fn test_lidarr_context_clue_provider_artists_block() {
let mut app = App::test_default();
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,
ActiveLidarrBlock::TestAllIndexers
)]
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() {
let mut blocks = EDIT_ARTIST_BLOCKS.to_vec();
blocks.extend(ADD_ROOT_FOLDER_BLOCKS);
blocks.extend(INDEXER_SETTINGS_BLOCKS);
blocks.extend(EDIT_INDEXER_BLOCKS);
for active_lidarr_block in blocks {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
}
#[test]
fn test_lidarr_context_clue_provider_add_artist_search_results_context_clues() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES);
}
#[rstest]
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_add_artist_blocks(
#[values(
ActiveLidarrBlock::AddArtistPrompt,
ActiveLidarrBlock::AddArtistSelectMonitor,
ActiveLidarrBlock::AddArtistSelectMonitorNewItems,
ActiveLidarrBlock::AddArtistSelectQualityProfile,
ActiveLidarrBlock::AddArtistSelectMetadataProfile,
ActiveLidarrBlock::AddArtistSelectRootFolder,
ActiveLidarrBlock::AddArtistTagsInput,
ActiveLidarrBlock::AddArtistAlreadyInLibrary
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[test]
fn test_lidarr_context_clue_provider_system_tasks_clues() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES);
}
}
+765
View File
@@ -0,0 +1,765 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::models::lidarr_models::{Album, Artist, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
use crate::models::servarr_models::Indexer;
use crate::network::NetworkEvent;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
album, artist, track,
};
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.data.lidarr_data.prompt_confirm = true;
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.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_lidarr_block_artist_details() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.artists.set_items(vec![artist()]);
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistDetails)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_artist_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistHistory)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetArtistHistory(1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_artist_search_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDiscographyReleases(1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_artist_search_block_discography_releases_non_empty() {
let mut app = App::test_default();
app
.data
.lidarr_data
.discography_releases
.set_items(vec![LidarrRelease::default()]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_album_details_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app.data.lidarr_data.albums.set_items(vec![Album {
id: 1,
..Album::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumDetails)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTracks(1, 1).into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTrackFiles(1).into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_album_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app.data.lidarr_data.albums.set_items(vec![Album {
id: 1,
..Album::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumHistory)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetAlbumHistory(1, 1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_album_history_block_no_op_when_albums_table_is_empty() {
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumHistory)
.await;
assert!(!app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_album_search_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app.data.lidarr_data.albums.set_items(vec![Album {
id: 1,
..Album::default()
}]);
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetAlbumReleases(1, 1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_album_search_block_is_loading() {
let mut app = App {
is_loading: true,
..App::test_default()
};
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
.await;
assert!(app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_album_search_block_album_releases_non_empty() {
let mut app = App::test_default();
let mut album_details_modal = AlbumDetailsModal::default();
album_details_modal
.album_releases
.set_items(vec![LidarrRelease::default()]);
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_downloads_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Downloads)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.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_dispatch_by_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::History)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetHistory(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_root_folders_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::RootFolders)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_indexers_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Indexers)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetIndexers.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_all_indexer_settings_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AllIndexerSettingsPrompt)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetAllIndexerSettings.into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_test_indexer_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.indexers.set_items(vec![Indexer {
id: 1,
..Indexer::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TestIndexer)
.await;
assert!(app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::TestIndexer(1).into());
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_test_all_indexers_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TestAllIndexers)
.await;
assert!(app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::TestAllIndexers.into()
);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::System)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTasks.into());
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetQueuedEvents.into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetLogs(500).into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_updates_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::SystemUpdates)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetUpdates.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_track_details_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default_fully_populated();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackDetails)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTrackDetails(1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_track_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default_fully_populated();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackHistory)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTrackHistory(1, 1, 1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_check_for_lidarr_prompt_action_no_prompt_confirm() {
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = false;
app.check_for_lidarr_prompt_action().await;
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.should_refresh);
}
#[tokio::test]
async fn test_check_for_lidarr_prompt_action() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::GetStatus);
app.check_for_lidarr_prompt_action().await;
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
assert!(app.should_refresh);
assert_eq!(app.data.lidarr_data.prompt_confirm_action, None);
}
#[tokio::test]
async fn test_lidarr_refresh_metadata() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.refresh_lidarr_metadata().await;
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::GetRootFolders.into());
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
assert!(app.is_loading);
}
#[tokio::test]
async fn test_lidarr_on_tick_first_render() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_first_render = true;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
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::GetRootFolders.into());
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
assert!(app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.is_first_render);
}
#[tokio::test]
async fn test_lidarr_on_tick_routing() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_lidarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.should_refresh = false;
app.is_first_render = false;
app.tick_count = 1;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert!(app.cancellation_token.is_cancelled());
}
#[tokio::test]
async fn test_lidarr_on_tick_should_refresh() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(app.should_refresh);
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_lidarr_on_tick_should_refresh_does_not_cancel_prompt_requests() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_loading = true;
app.is_routing = true;
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.cancellation_token.is_cancelled());
}
#[tokio::test]
async fn test_lidarr_on_tick_network_tick_frequency() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.tick_count = 2;
app.tick_until_poll = 2;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
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::GetRootFolders.into());
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
}
#[tokio::test]
async fn test_extract_add_new_artist_search_query() {
let app = App::test_default_fully_populated();
let query = app.extract_add_new_artist_search_query().await;
assert_str_eq!(query, "Test Artist");
}
#[tokio::test]
#[should_panic(expected = "Add artist search is empty")]
async fn test_extract_add_new_artist_search_query_panics_when_the_query_is_not_set() {
let app = App::test_default();
app.extract_add_new_artist_search_query().await;
}
#[tokio::test]
async fn test_extract_artist_id() {
let mut app = App::test_default();
app.data.lidarr_data.artists.set_items(vec![artist()]);
assert_eq!(app.extract_artist_id().await, 1);
}
#[tokio::test]
async fn test_extract_album_id() {
let mut app = App::test_default();
app.data.lidarr_data.albums.set_items(vec![album()]);
assert_eq!(app.extract_album_id().await, 1);
}
#[tokio::test]
async fn test_extract_track_id() {
let mut app = App::test_default();
let mut album_details_modal = AlbumDetailsModal::default();
album_details_modal.tracks.set_items(vec![track()]);
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
assert_eq!(app.extract_track_id().await, 1);
}
#[tokio::test]
#[should_panic(expected = "album_details_modal is empty")]
async fn test_extract_track_id_panics_when_album_details_modal_is_not_set() {
let app = App::test_default();
app.extract_track_id().await;
}
#[tokio::test]
async fn test_extract_lidarr_indexer_id() {
let mut app = App::test_default();
app.data.lidarr_data.indexers.set_items(vec![Indexer {
id: 1,
..Indexer::default()
}]);
assert_eq!(app.extract_lidarr_indexer_id().await, 1);
}
}
+274
View File
@@ -0,0 +1,274 @@
use super::App;
use crate::{
models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
network::lidarr_network::LidarrEvent,
};
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::Downloads => {
self
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
.await;
}
ActiveLidarrBlock::ArtistDetails => {
self
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
.await;
}
ActiveLidarrBlock::ArtistHistory => {
self
.dispatch_network_event(
LidarrEvent::GetArtistHistory(self.extract_artist_id().await).into(),
)
.await;
}
ActiveLidarrBlock::ManualArtistSearch => {
if self.data.lidarr_data.discography_releases.is_empty() {
self
.dispatch_network_event(
LidarrEvent::GetDiscographyReleases(self.extract_artist_id().await).into(),
)
.await;
}
}
ActiveLidarrBlock::AlbumDetails => {
let artist_id = self.extract_artist_id().await;
let album_id = self.extract_album_id().await;
self
.dispatch_network_event(LidarrEvent::GetTracks(artist_id, album_id).into())
.await;
self
.dispatch_network_event(LidarrEvent::GetTrackFiles(album_id).into())
.await;
self
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
.await;
}
ActiveLidarrBlock::AlbumHistory => {
if !self.data.lidarr_data.albums.is_empty() {
self
.dispatch_network_event(
LidarrEvent::GetAlbumHistory(
self.extract_artist_id().await,
self.extract_album_id().await,
)
.into(),
)
.await;
}
}
ActiveLidarrBlock::ManualAlbumSearch => {
match self.data.lidarr_data.album_details_modal.as_ref() {
Some(album_details_modal) if album_details_modal.album_releases.is_empty() => {
self
.dispatch_network_event(
LidarrEvent::GetAlbumReleases(
self.extract_artist_id().await,
self.extract_album_id().await,
)
.into(),
)
.await;
}
_ => (),
}
}
ActiveLidarrBlock::AddArtistSearchResults => {
self
.dispatch_network_event(
LidarrEvent::SearchNewArtist(self.extract_add_new_artist_search_query().await).into(),
)
.await;
}
ActiveLidarrBlock::History => {
self
.dispatch_network_event(LidarrEvent::GetHistory(500).into())
.await
}
ActiveLidarrBlock::RootFolders => {
self
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
.await;
}
ActiveLidarrBlock::Indexers => {
self
.dispatch_network_event(LidarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetIndexers.into())
.await;
}
ActiveLidarrBlock::AllIndexerSettingsPrompt => {
self
.dispatch_network_event(LidarrEvent::GetAllIndexerSettings.into())
.await;
}
ActiveLidarrBlock::TestIndexer => {
self
.dispatch_network_event(
LidarrEvent::TestIndexer(self.extract_lidarr_indexer_id().await).into(),
)
.await;
}
ActiveLidarrBlock::TestAllIndexers => {
self
.dispatch_network_event(LidarrEvent::TestAllIndexers.into())
.await;
}
ActiveLidarrBlock::System => {
self
.dispatch_network_event(LidarrEvent::GetTasks.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetQueuedEvents.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetLogs(500).into())
.await;
}
ActiveLidarrBlock::SystemUpdates => {
self
.dispatch_network_event(LidarrEvent::GetUpdates.into())
.await;
}
ActiveLidarrBlock::TrackDetails => {
self
.dispatch_network_event(
LidarrEvent::GetTrackDetails(self.extract_track_id().await).into(),
)
.await;
}
ActiveLidarrBlock::TrackHistory => {
let artist_id = self.extract_artist_id().await;
let album_id = self.extract_album_id().await;
let track_id = self.extract_track_id().await;
self
.dispatch_network_event(
LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).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 is empty")
.text
.clone()
}
async fn extract_artist_id(&self) -> i64 {
self.data.lidarr_data.artists.current_selection().id
}
async fn extract_album_id(&self) -> i64 {
self.data.lidarr_data.albums.current_selection().id
}
async fn extract_track_id(&self) -> i64 {
self
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("album_details_modal is empty")
.tracks
.current_selection()
.id
}
async fn extract_lidarr_indexer_id(&self) -> i64 {
self.data.lidarr_data.indexers.current_selection().id
}
async fn check_for_lidarr_prompt_action(&mut self) {
if self.data.lidarr_data.prompt_confirm {
self.data.lidarr_data.prompt_confirm = false;
if let Some(lidarr_event) = self.data.lidarr_data.prompt_confirm_action.take() {
self.dispatch_network_event(lidarr_event.into()).await;
self.should_refresh = true;
}
}
}
pub(super) async fn lidarr_on_tick(&mut self, active_lidarr_block: ActiveLidarrBlock) {
if self.is_first_render {
self.refresh_lidarr_metadata().await;
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
self.is_first_render = false;
return;
}
if self.should_refresh {
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
self.refresh_lidarr_metadata().await;
}
if self.is_routing {
if !self.should_refresh {
self.cancellation_token.cancel();
} else {
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
}
}
if self.tick_count.is_multiple_of(self.tick_until_poll) {
self.refresh_lidarr_metadata().await;
}
}
async fn refresh_lidarr_metadata(&mut self) {
self
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
.await;
self
.dispatch_network_event(LidarrEvent::GetDiskSpace.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetStatus.into())
.await;
}
}
+64 -2
View File
@@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken;
use veil::Redact; use veil::Redact;
use crate::cli::Command; use crate::cli::Command;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::servarr_models::KeybindingItem; use crate::models::servarr_models::KeybindingItem;
@@ -25,6 +26,7 @@ mod app_tests;
pub mod context_clues; pub mod context_clues;
pub mod key_binding; pub mod key_binding;
mod key_binding_tests; mod key_binding_tests;
pub mod lidarr;
pub mod radarr; pub mod radarr;
pub mod sonarr; pub mod sonarr;
@@ -39,6 +41,7 @@ pub struct App<'a> {
pub tick_until_poll: u64, pub tick_until_poll: u64,
pub ticks_until_scroll: u64, pub ticks_until_scroll: u64,
pub tick_count: u64, pub tick_count: u64,
pub ui_scroll_tick_count: u64,
pub is_routing: bool, pub is_routing: bool,
pub is_loading: bool, pub is_loading: bool,
pub should_refresh: bool, pub should_refresh: bool,
@@ -95,6 +98,26 @@ impl App<'_> {
server_tabs.extend(sonarr_tabs); server_tabs.extend(sonarr_tabs);
} }
if let Some(lidarr_configs) = config.lidarr {
let mut unnamed_idx = 0;
let lidarr_tabs = lidarr_configs.into_iter().map(|lidarr_config| {
let name = if let Some(name) = lidarr_config.name.clone() {
name
} else {
unnamed_idx += 1;
format!("Lidarr {unnamed_idx}")
};
TabRoute {
title: name,
route: ActiveLidarrBlock::Artists.into(),
contextual_help: None,
config: Some(lidarr_config),
}
});
server_tabs.extend(lidarr_tabs);
}
let weight_sorted_tabs = server_tabs let weight_sorted_tabs = server_tabs
.into_iter() .into_iter()
.sorted_by(|tab1, tab2| { .sorted_by(|tab1, tab2| {
@@ -145,6 +168,14 @@ impl App<'_> {
self.tick_count = 0; self.tick_count = 0;
} }
pub fn on_ui_scroll_tick(&mut self) {
if self.ui_scroll_tick_count == self.ticks_until_scroll {
self.ui_scroll_tick_count = 0;
} else {
self.ui_scroll_tick_count += 1;
}
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.reset_tick_count(); self.reset_tick_count();
@@ -167,6 +198,7 @@ impl App<'_> {
match self.get_current_route() { match self.get_current_route() {
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await, Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await, Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await,
_ => (), _ => (),
} }
@@ -225,8 +257,9 @@ impl Default for App<'_> {
is_first_render: true, is_first_render: true,
server_tabs: TabState::new(Vec::new()), server_tabs: TabState::new(Vec::new()),
tick_until_poll: 400, tick_until_poll: 400,
ticks_until_scroll: 4, ticks_until_scroll: 64,
tick_count: 0, tick_count: 0,
ui_scroll_tick_count: 0,
is_loading: false, is_loading: false,
is_routing: false, is_routing: false,
should_refresh: false, should_refresh: false,
@@ -254,6 +287,12 @@ impl App<'_> {
contextual_help: None, contextual_help: None,
config: Some(ServarrConfig::default()), config: Some(ServarrConfig::default()),
}, },
TabRoute {
title: "Lidarr".to_owned(),
route: ActiveLidarrBlock::Artists.into(),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
]), ]),
..App::default() ..App::default()
} }
@@ -262,6 +301,7 @@ impl App<'_> {
pub fn test_default_fully_populated() -> Self { pub fn test_default_fully_populated() -> Self {
App { App {
data: Data { data: Data {
lidarr_data: LidarrData::test_default_fully_populated(),
radarr_data: RadarrData::test_default_fully_populated(), radarr_data: RadarrData::test_default_fully_populated(),
sonarr_data: SonarrData::test_default_fully_populated(), sonarr_data: SonarrData::test_default_fully_populated(),
}, },
@@ -278,6 +318,12 @@ impl App<'_> {
contextual_help: None, contextual_help: None,
config: Some(ServarrConfig::default()), config: Some(ServarrConfig::default()),
}, },
TabRoute {
title: "Lidarr".to_owned(),
route: ActiveLidarrBlock::Artists.into(),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
]), ]),
..App::default() ..App::default()
} }
@@ -286,6 +332,7 @@ impl App<'_> {
#[derive(Default)] #[derive(Default)]
pub struct Data<'a> { pub struct Data<'a> {
pub lidarr_data: LidarrData<'a>,
pub radarr_data: RadarrData<'a>, pub radarr_data: RadarrData<'a>,
pub sonarr_data: SonarrData<'a>, pub sonarr_data: SonarrData<'a>,
} }
@@ -293,13 +340,14 @@ pub struct Data<'a> {
#[derive(Debug, Deserialize, Serialize, Default, Clone)] #[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AppConfig { pub struct AppConfig {
pub theme: Option<String>, pub theme: Option<String>,
pub lidarr: Option<Vec<ServarrConfig>>,
pub radarr: Option<Vec<ServarrConfig>>, pub radarr: Option<Vec<ServarrConfig>>,
pub sonarr: Option<Vec<ServarrConfig>>, pub sonarr: Option<Vec<ServarrConfig>>,
} }
impl AppConfig { impl AppConfig {
pub fn validate(&self) { pub fn validate(&self) {
if self.radarr.is_none() && self.sonarr.is_none() { if self.lidarr.is_none() && self.radarr.is_none() && self.sonarr.is_none() {
log_and_print_error( log_and_print_error(
"No Servarr configuration provided in the specified configuration file".to_owned(), "No Servarr configuration provided in the specified configuration file".to_owned(),
); );
@@ -313,6 +361,10 @@ impl AppConfig {
if let Some(sonarr_configs) = &self.sonarr { if let Some(sonarr_configs) = &self.sonarr {
sonarr_configs.iter().for_each(|config| config.validate()); sonarr_configs.iter().for_each(|config| config.validate());
} }
if let Some(lidarr_configs) = &self.lidarr {
lidarr_configs.iter().for_each(|config| config.validate());
}
} }
pub fn verify_config_present_for_cli(&self, command: &Command) { pub fn verify_config_present_for_cli(&self, command: &Command) {
@@ -330,6 +382,10 @@ impl AppConfig {
msg("Sonarr"); msg("Sonarr");
process::exit(1); process::exit(1);
} }
Command::Lidarr(_) if self.lidarr.is_none() => {
msg("Lidarr");
process::exit(1);
}
_ => (), _ => (),
} }
} }
@@ -346,6 +402,12 @@ impl AppConfig {
sonarr_config.post_process_initialization(); sonarr_config.post_process_initialization();
} }
} }
if let Some(lidarr_configs) = self.lidarr.as_mut() {
for lidarr_config in lidarr_configs {
lidarr_config.post_process_initialization();
}
}
} }
} }
+5
View File
@@ -62,6 +62,11 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetDownloads(500).into()) .dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await; .await;
} }
ActiveRadarrBlock::History => {
self
.dispatch_network_event(RadarrEvent::GetHistory(500).into())
.await;
}
ActiveRadarrBlock::Indexers => { ActiveRadarrBlock::Indexers => {
self self
.dispatch_network_event(RadarrEvent::GetTags.into()) .dispatch_network_event(RadarrEvent::GetTags.into())
+1 -5
View File
@@ -1,6 +1,7 @@
use crate::app::App; use crate::app::App;
use crate::app::context_clues::{ use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
}; };
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::models::Route; use crate::models::Route;
@@ -82,11 +83,6 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"), (DEFAULT_KEYBINDINGS.esc, "edit search"),
]; ];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.submit, "show overview/add movie"), (DEFAULT_KEYBINDINGS.submit, "show overview/add movie"),
(DEFAULT_KEYBINDINGS.edit, "edit collection"), (DEFAULT_KEYBINDINGS.edit, "edit collection"),
+8 -6
View File
@@ -3,14 +3,15 @@ mod tests {
use crate::app::App; use crate::app::App;
use crate::app::context_clues::{ use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
SYSTEM_TASKS_CONTEXT_CLUES,
}; };
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{ use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES, ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES,
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider, SYSTEM_TASKS_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider,
}; };
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
@@ -459,9 +460,10 @@ mod tests {
#[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)] #[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)]
#[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)] #[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)]
#[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)] #[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)] #[case(4, ActiveRadarrBlock::History, &HISTORY_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)] #[case(5, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
#[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)] #[case(6, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
#[case(7, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
fn test_radarr_context_clue_provider_radarr_blocks_context_clues( fn test_radarr_context_clue_provider_radarr_blocks_context_clues(
#[case] index: usize, #[case] index: usize,
#[case] active_radarr_block: ActiveRadarrBlock, #[case] active_radarr_block: ActiveRadarrBlock,
+21 -3
View File
@@ -6,7 +6,8 @@ mod tests {
use crate::app::App; use crate::app::App;
use crate::app::radarr::ActiveRadarrBlock; use crate::app::radarr::ActiveRadarrBlock;
use crate::models::radarr_models::{ use crate::models::radarr_models::{
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease, AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, MinimumAvailability, Movie,
MovieMonitor, RadarrRelease,
}; };
use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_models::Indexer; use crate::models::servarr_models::Indexer;
@@ -88,13 +89,13 @@ mod tests {
tmdb_id: 1234, tmdb_id: 1234,
title: "Test".to_owned(), title: "Test".to_owned(),
root_folder_path: "/nfs2".to_owned(), root_folder_path: "/nfs2".to_owned(),
minimum_availability: "announced".to_owned(), minimum_availability: MinimumAvailability::Announced,
monitored: true, monitored: true,
quality_profile_id: 2222, quality_profile_id: 2222,
tags: vec![1, 2], tags: vec![1, 2],
tag_input_string: None, tag_input_string: None,
add_options: AddMovieOptions { add_options: AddMovieOptions {
monitor: "movieOnly".to_owned(), monitor: MovieMonitor::MovieOnly,
search_for_movie: true, search_for_movie: true,
}, },
}; };
@@ -146,6 +147,23 @@ mod tests {
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
} }
#[tokio::test]
async fn test_dispatch_by_history_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_radarr_block(&ActiveRadarrBlock::History)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetHistory(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test] #[tokio::test]
async fn test_dispatch_by_root_folders_block() { async fn test_dispatch_by_root_folders_block() {
let (mut app, mut sync_network_rx) = construct_app_unit(); let (mut app, mut sync_network_rx) = construct_app_unit();
+1 -17
View File
@@ -1,5 +1,6 @@
use crate::app::context_clues::{ use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
}; };
use crate::app::{App, context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS}; use crate::app::{App, context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS};
use crate::models::Route; use crate::models::Route;
@@ -57,18 +58,6 @@ pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [ pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
@@ -175,11 +164,6 @@ pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub(in crate::app) struct SonarrContextClueProvider; pub(in crate::app) struct SonarrContextClueProvider;
impl ContextClueProvider for SonarrContextClueProvider { impl ContextClueProvider for SonarrContextClueProvider {
+6 -41
View File
@@ -2,8 +2,9 @@
mod tests { mod tests {
use crate::app::context_clues::{ use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
SYSTEM_TASKS_CONTEXT_CLUES,
}; };
use crate::app::sonarr::sonarr_context_clues::{ use crate::app::sonarr::sonarr_context_clues::{
SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, SonarrContextClueProvider, SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, SonarrContextClueProvider,
@@ -13,10 +14,9 @@ mod tests {
key_binding::DEFAULT_KEYBINDINGS, key_binding::DEFAULT_KEYBINDINGS,
sonarr::sonarr_context_clues::{ sonarr::sonarr_context_clues::{
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES,
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES,
SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES,
SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
}, },
}; };
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
@@ -146,40 +146,6 @@ mod tests {
assert_none!(series_history_context_clues_iter.next()); assert_none!(series_history_context_clues_iter.next());
} }
#[test]
fn test_history_context_clues() {
let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_none!(history_context_clues_iter.next());
}
#[test] #[test]
fn test_series_details_context_clues() { fn test_series_details_context_clues() {
let mut series_details_context_clues_iter = SERIES_DETAILS_CONTEXT_CLUES.iter(); let mut series_details_context_clues_iter = SERIES_DETAILS_CONTEXT_CLUES.iter();
@@ -455,7 +421,6 @@ mod tests {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::default().into()); app.push_navigation_stack(ActiveRadarrBlock::default().into());
// This should panic because the route is not a Sonarr route
SonarrContextClueProvider::get_context_clues(&mut app); SonarrContextClueProvider::get_context_clues(&mut app);
} }
+9
View File
@@ -55,6 +55,13 @@ mod tests {
assert_ok!(&result); assert_ok!(&result);
} }
#[test]
fn test_lidarr_subcommand_delegates_to_lidarr() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
assert_ok!(&result);
}
#[test] #[test]
fn test_completions_requires_argument() { fn test_completions_requires_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions"]); let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
@@ -174,4 +181,6 @@ mod tests {
assert_ok!(&result); assert_ok!(&result);
} }
// TODO: Implement test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler
} }
+223
View File
@@ -0,0 +1,223 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, Subcommand, arg};
use tokio::sync::Mutex;
use super::LidarrCommand;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::lidarr_models::{
AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, MonitorType, NewItemMonitorType,
},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
#[cfg(test)]
#[path = "add_command_handler_tests.rs"]
mod add_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrAddCommand {
#[command(about = "Add a new artist to your Lidarr library")]
Artist {
#[arg(
long,
help = "The MusicBrainz foreign artist ID of the artist you wish to add to your library",
required = true
)]
foreign_artist_id: String,
#[arg(long, help = "The name of the artist", required = true)]
artist_name: String,
#[arg(
long,
help = "The root folder path where all artist data and metadata should live",
required = true
)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the quality profile to use for this artist",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the metadata profile to use for this artist",
required = true
)]
metadata_profile_id: i64,
#[arg(long, help = "Disable monitoring for this artist")]
disable_monitoring: bool,
#[arg(
long,
help = "Tag IDs to tag the artist with",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
#[arg(
long,
help = "What Lidarr should monitor for this artist",
value_enum,
default_value_t = MonitorType::default()
)]
monitor: MonitorType,
#[arg(
long,
help = "How Lidarr should monitor new items for this artist",
value_enum,
default_value_t = NewItemMonitorType::default()
)]
monitor_new_items: NewItemMonitorType,
#[arg(
long,
help = "Tell Lidarr to not start a search for missing albums once the artist is added to your library"
)]
no_search_for_missing_albums: bool,
},
#[command(about = "Add a new root folder")]
RootFolder {
#[arg(long, help = "The name of the root folder", required = true)]
name: String,
#[arg(long, help = "The path of the new root folder", required = true)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the default quality profile for artists in this root folder",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the default metadata profile for artists in this root folder",
required = true
)]
metadata_profile_id: i64,
#[arg(
long,
help = "The default monitor option for artists in this root folder",
value_enum,
default_value_t = MonitorType::default()
)]
monitor: MonitorType,
#[arg(
long,
help = "The default monitor new items option for artists in this root folder",
value_enum,
default_value_t = NewItemMonitorType::default()
)]
monitor_new_items: NewItemMonitorType,
#[arg(
long,
help = "Default tag IDs for artists in this root folder",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
},
#[command(about = "Add new tag")]
Tag {
#[arg(long, help = "The name of the tag to be added", required = true)]
name: String,
},
}
impl From<LidarrAddCommand> for Command {
fn from(value: LidarrAddCommand) -> Self {
Command::Lidarr(LidarrCommand::Add(value))
}
}
pub(super) struct LidarrAddCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrAddCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrAddCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrAddCommandHandler {
_app: app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrAddCommand::Artist {
foreign_artist_id,
artist_name,
root_folder_path,
quality_profile_id,
metadata_profile_id,
disable_monitoring,
tag: tags,
monitor,
monitor_new_items,
no_search_for_missing_albums,
} => {
let body = AddArtistBody {
foreign_artist_id,
artist_name,
monitored: !disable_monitoring,
root_folder_path,
quality_profile_id,
metadata_profile_id,
tags,
tag_input_string: None,
add_options: AddArtistOptions {
monitor,
monitor_new_items,
search_for_missing_albums: !no_search_for_missing_albums,
},
};
let resp = self
.network
.handle_network_event(LidarrEvent::AddArtist(body).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrAddCommand::RootFolder {
name,
root_folder_path,
quality_profile_id,
metadata_profile_id,
monitor,
monitor_new_items,
tag: tags,
} => {
let add_root_folder_body = AddLidarrRootFolderBody {
name,
path: root_folder_path,
default_quality_profile_id: quality_profile_id,
default_metadata_profile_id: metadata_profile_id,
default_monitor_option: monitor,
default_new_item_monitor_option: monitor_new_items,
default_tags: tags,
tag_input_string: None,
};
let resp = self
.network
.handle_network_event(LidarrEvent::AddRootFolder(add_root_folder_body).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrAddCommand::Tag { name } => {
let resp = self
.network
.handle_network_event(LidarrEvent::AddTag(name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
+561
View File
@@ -0,0 +1,561 @@
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser, error::ErrorKind};
use crate::{
Cli,
cli::{
Command,
lidarr::{LidarrCommand, add_command_handler::LidarrAddCommand},
},
models::lidarr_models::{MonitorType, NewItemMonitorType},
};
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_add_command_from() {
let command = LidarrAddCommand::Tag {
name: String::new(),
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Add(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_add_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "root-folder"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_root_folder_success() {
let expected_args = LidarrAddCommand::RootFolder {
name: "Music".to_owned(),
root_folder_path: "/nfs/test".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
tag: vec![],
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"root-folder",
"--name",
"Music",
"--root-folder-path",
"/nfs/test",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "tag"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_tag_success() {
let expected_args = LidarrAddCommand::Tag {
name: "test".to_owned(),
};
let result = Cli::try_parse_from(["managarr", "lidarr", "add", "tag", "--name", "test"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_foreign_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_artist_name() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_root_folder_path() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_quality_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_metadata_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_success_with_required_args_only() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
disable_monitoring: false,
tag: vec![],
monitor: MonitorType::default(),
monitor_new_items: NewItemMonitorType::default(),
no_search_for_missing_albums: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_success_with_all_args() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 2,
disable_monitoring: true,
tag: vec![1, 2],
monitor: MonitorType::Future,
monitor_new_items: NewItemMonitorType::New,
no_search_for_missing_albums: true,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--disable-monitoring",
"--tag",
"1",
"--tag",
"2",
"--monitor",
"future",
"--monitor-new-items",
"new",
"--no-search-for-missing-albums",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_monitor_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--monitor",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_artist_new_item_monitor_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--monitor-new-items",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_artist_tags_is_repeatable() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 2,
disable_monitoring: false,
tag: vec![1, 2],
monitor: MonitorType::default(),
monitor_new_items: NewItemMonitorType::default(),
no_search_for_missing_albums: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
use crate::models::Serdeable;
use crate::models::lidarr_models::{
AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, LidarrSerdeable, MonitorType,
NewItemMonitorType,
};
use crate::network::lidarr_network::LidarrEvent;
use crate::{
app::App,
network::{MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_add_root_folder_command() {
let expected_root_folder_path = "/nfs/test".to_owned();
let expected_add_root_folder_body = AddLidarrRootFolderBody {
name: "Music".to_owned(),
path: expected_root_folder_path.clone(),
default_quality_profile_id: 1,
default_metadata_profile_id: 1,
default_monitor_option: MonitorType::All,
default_new_item_monitor_option: NewItemMonitorType::All,
default_tags: vec![1, 2],
tag_input_string: None,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddRootFolder(expected_add_root_folder_body.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_root_folder_command = LidarrAddCommand::RootFolder {
name: "Music".to_owned(),
root_folder_path: expected_root_folder_path,
quality_profile_id: 1,
metadata_profile_id: 1,
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
tag: vec![1, 2],
};
let result =
LidarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_add_tag_command() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = LidarrAddCommand::Tag {
name: expected_tag_name,
};
let result = LidarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_add_artist_command() {
let expected_body = AddArtistBody {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
monitored: false,
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddArtistOptions {
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
search_for_missing_albums: false,
},
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddArtist(expected_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_artist_command = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
disable_monitoring: true,
tag: vec![1, 2],
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
no_search_for_missing_albums: true,
};
let result = LidarrAddCommandHandler::with(&app_arc, add_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
+165
View File
@@ -0,0 +1,165 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::lidarr_models::DeleteParams,
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "delete_command_handler_tests.rs"]
mod delete_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrDeleteCommand {
#[command(about = "Delete an album from your Lidarr library")]
Album {
#[arg(long, help = "The ID of the album to delete", required = true)]
album_id: i64,
#[arg(long, help = "Delete the album files from disk as well")]
delete_files_from_disk: bool,
#[arg(long, help = "Add a list exclusion for this album")]
add_list_exclusion: bool,
},
#[command(about = "Delete the specified track file from disk")]
TrackFile {
#[arg(long, help = "The ID of the track file to delete", required = true)]
track_file_id: i64,
},
#[command(about = "Delete an artist from your Lidarr library")]
Artist {
#[arg(long, help = "The ID of the artist to delete", required = true)]
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 specified download")]
Download {
#[arg(long, help = "The ID of the download to delete", required = true)]
download_id: i64,
},
#[command(about = "Delete the indexer with the given ID")]
Indexer {
#[arg(long, help = "The ID of the indexer to delete", required = true)]
indexer_id: i64,
},
#[command(about = "Delete the root folder with the given ID")]
RootFolder {
#[arg(long, help = "The ID of the root folder to delete", required = true)]
root_folder_id: i64,
},
#[command(about = "Delete the tag with the specified ID")]
Tag {
#[arg(long, help = "The ID of the tag to delete", required = true)]
tag_id: i64,
},
}
impl From<LidarrDeleteCommand> for Command {
fn from(value: LidarrDeleteCommand) -> Self {
Command::Lidarr(LidarrCommand::Delete(value))
}
}
pub(super) struct LidarrDeleteCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrDeleteCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrDeleteCommand::Album {
album_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_album_params = DeleteParams {
id: album_id,
delete_files: delete_files_from_disk,
add_import_list_exclusion: add_list_exclusion,
};
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteAlbum(delete_album_params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::TrackFile { track_file_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteTrackFile(track_file_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Artist {
artist_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_artist_params = DeleteParams {
id: artist_id,
delete_files: delete_files_from_disk,
add_import_list_exclusion: add_list_exclusion,
};
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteArtist(delete_artist_params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Download { download_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteDownload(download_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Indexer { indexer_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteIndexer(indexer_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::RootFolder { root_folder_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteRootFolder(root_folder_id).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,528 @@
#[cfg(test)]
mod tests {
use crate::{
Cli,
cli::{
Command,
lidarr::{LidarrCommand, delete_command_handler::LidarrDeleteCommand},
},
};
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_delete_command_from() {
let command = LidarrDeleteCommand::Artist {
artist_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Delete(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_delete_album_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "album"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_album_defaults() {
let expected_args = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result =
Cli::try_parse_from(["managarr", "lidarr", "delete", "album", "--album-id", "1"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_album_all_args_defined() {
let expected_args = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"album",
"--album-id",
"1",
"--delete-files-from-disk",
"--add-list-exclusion",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_track_file_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "track-file"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_track_file_success() {
let expected_args = LidarrDeleteCommand::TrackFile { track_file_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"track-file",
"--track-file-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]);
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_download_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "download"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_download_success() {
let expected_args = LidarrDeleteCommand::Download { download_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"download",
"--download-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_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "indexer"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_indexer_success() {
let expected_args = LidarrDeleteCommand::Indexer { indexer_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"indexer",
"--indexer-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_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "root-folder"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_root_folder_success() {
let expected_args = LidarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"root-folder",
"--root-folder-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_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_tag_success() {
let expected_args = LidarrDeleteCommand::Tag { tag_id: 1 };
let result = Cli::try_parse_from(["managarr", "lidarr", "delete", "tag", "--tag-id", "1"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
CliCommandHandler,
lidarr::delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler},
},
models::{
Serdeable,
lidarr_models::{DeleteParams, LidarrSerdeable},
},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_delete_album_command() {
let expected_delete_album_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteAlbum(expected_delete_album_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_album_command = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_album_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_track_file_command() {
let expected_track_file_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteTrackFile(expected_track_file_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_track_file_command = LidarrDeleteCommand::TrackFile { track_file_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_track_file_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_artist_command() {
let expected_delete_artist_params = DeleteParams {
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_download_command() {
let expected_download_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteDownload(expected_download_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_download_command = LidarrDeleteCommand::Download { download_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_indexer_command() {
let expected_indexer_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteIndexer(expected_indexer_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_indexer_command = LidarrDeleteCommand::Indexer { indexer_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_root_folder_command() {
let expected_root_folder_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteRootFolder(expected_root_folder_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_root_folder_command = LidarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_root_folder_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);
}
}
}
+343
View File
@@ -0,0 +1,343 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, ArgGroup, Subcommand};
use tokio::sync::Mutex;
use super::LidarrCommand;
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
use crate::{
app::App,
cli::{CliCommandHandler, Command, mutex_flags_or_option},
models::lidarr_models::{EditArtistParams, NewItemMonitorType},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
#[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 and indexer settings that apply to all indexers",
group(
ArgGroup::new("edit_settings")
.args([
"maximum_size",
"minimum_age",
"retention",
"rss_sync_interval",
]).required(true)
.multiple(true))
)]
AllIndexerSettings {
#[arg(
long,
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
)]
maximum_size: Option<i64>,
#[arg(
long,
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
)]
minimum_age: Option<i64>,
#[arg(
long,
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
)]
retention: Option<i64>,
#[arg(
long,
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
)]
rss_sync_interval: Option<i64>,
},
#[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,
},
#[command(
about = "Edit preferences for the specified indexer",
group(
ArgGroup::new("edit_indexer")
.args([
"name",
"enable_rss",
"disable_rss",
"enable_automatic_search",
"disable_automatic_search",
"enable_interactive_search",
"disable_automatic_search",
"url",
"api_key",
"seed_ratio",
"tag",
"priority",
"clear_tags"
]).required(true)
.multiple(true))
)]
Indexer {
#[arg(
long,
help = "The ID of the indexer whose settings you wish to edit",
required = true
)]
indexer_id: i64,
#[arg(long, help = "The name of the indexer")]
name: Option<String>,
#[arg(
long,
help = "Indicate to Lidarr that this indexer should be used when Lidarr periodically looks for releases via RSS Sync",
conflicts_with = "disable_rss"
)]
enable_rss: bool,
#[arg(
long,
help = "Disable using this indexer when Lidarr periodically looks for releases via RSS Sync",
conflicts_with = "enable_rss"
)]
disable_rss: bool,
#[arg(
long,
help = "Indicate to Lidarr that this indexer should be used when automatic searches are performed via the UI or by Lidarr",
conflicts_with = "disable_automatic_search"
)]
enable_automatic_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Lidarr",
conflicts_with = "enable_automatic_search"
)]
disable_automatic_search: bool,
#[arg(
long,
help = "Indicate to Lidarr that this indexer should be used when an interactive search is used",
conflicts_with = "disable_interactive_search"
)]
enable_interactive_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever an interactive search is performed",
conflicts_with = "enable_interactive_search"
)]
disable_interactive_search: bool,
#[arg(long, help = "The URL of the indexer")]
url: Option<String>,
#[arg(long, help = "The API key used to access the indexer's API")]
api_key: Option<String>,
#[arg(
long,
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
)]
seed_ratio: Option<String>,
#[arg(
long,
help = "Only use this indexer for series with at least one matching tag ID. Leave blank to use with all series.",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(
long,
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Lidarr will still use all enabled indexers for RSS Sync and Searching"
)]
priority: Option<i64>,
#[arg(long, help = "Clear all tags on this indexer", 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::AllIndexerSettings {
maximum_size,
minimum_age,
retention,
rss_sync_interval,
} => {
if let Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(previous_indexer_settings)) = self
.network
.handle_network_event(LidarrEvent::GetAllIndexerSettings.into())
.await?
{
let params = IndexerSettings {
id: 1,
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
retention: retention.unwrap_or(previous_indexer_settings.retention),
rss_sync_interval: rss_sync_interval
.unwrap_or(previous_indexer_settings.rss_sync_interval),
};
self
.network
.handle_network_event(LidarrEvent::EditAllIndexerSettings(params).into())
.await?;
"All indexer settings updated".to_owned()
} else {
String::new()
}
}
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()
}
LidarrEditCommand::Indexer {
indexer_id,
name,
enable_rss,
disable_rss,
enable_automatic_search,
disable_automatic_search,
enable_interactive_search,
disable_interactive_search,
url,
api_key,
seed_ratio,
tag,
priority,
clear_tags,
} => {
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
let automatic_search_value =
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
let interactive_search_value =
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
let edit_indexer_params = EditIndexerParams {
indexer_id,
name,
enable_rss: rss_value,
enable_automatic_search: automatic_search_value,
enable_interactive_search: interactive_search_value,
url,
api_key,
seed_ratio,
tags: tag,
tag_input_string: None,
priority,
clear_tags,
};
self
.network
.handle_network_event(LidarrEvent::EditIndexer(edit_indexer_params).into())
.await?;
"Indexer updated".to_owned()
}
};
Ok(result)
}
}
@@ -0,0 +1,858 @@
#[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_all_indexer_settings_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "all-indexer-settings"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_edit_all_indexer_settings_assert_argument_flags_require_args(
#[values(
"--maximum-size",
"--minimum-age",
"--retention",
"--rss-sync-interval"
)]
flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"all-indexer-settings",
flag,
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() {
let expected_args = LidarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: None,
retention: None,
rss_sync_interval: None,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"all-indexer-settings",
"--maximum-size",
"1",
]);
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_all_indexer_settings_all_arguments_defined() {
let expected_args = LidarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"all-indexer-settings",
"--maximum-size",
"1",
"--minimum-age",
"1",
"--retention",
"1",
"--rss-sync-interval",
"1",
]);
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_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);
}
#[test]
fn test_edit_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "indexer"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_indexer_with_indexer_id_still_requires_arguments() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_indexer_rss_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-rss",
"--disable-rss",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_automatic_search_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-automatic-search",
"--disable-automatic-search",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_interactive_search_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-interactive-search",
"--disable-interactive-search",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_tag_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--tag",
"1",
"--clear-tags",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[rstest]
fn test_edit_indexer_assert_argument_flags_require_args(
#[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
flag,
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() {
let expected_args = LidarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: false,
disable_rss: false,
enable_automatic_search: false,
disable_automatic_search: false,
enable_interactive_search: false,
disable_interactive_search: false,
url: None,
api_key: None,
seed_ratio: None,
tag: None,
priority: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--name",
"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_indexer_tag_argument_is_repeatable() {
let expected_args = LidarrEditCommand::Indexer {
indexer_id: 1,
name: None,
enable_rss: false,
disable_rss: false,
enable_automatic_search: false,
disable_automatic_search: false,
enable_interactive_search: false,
disable_interactive_search: false,
url: None,
api_key: None,
seed_ratio: None,
tag: Some(vec![1, 2]),
priority: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-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_indexer_all_arguments_defined() {
let expected_args = LidarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: true,
disable_rss: false,
enable_automatic_search: true,
disable_automatic_search: false,
enable_interactive_search: true,
disable_interactive_search: false,
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tag: Some(vec![1, 2]),
priority: Some(25),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--name",
"Test",
"--enable-rss",
"--enable-automatic-search",
"--enable-interactive-search",
"--url",
"http://test.com",
"--api-key",
"testKey",
"--seed-ratio",
"1.2",
"--tag",
"1",
"--tag",
"2",
"--priority",
"25",
]);
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::models::servarr_models::{EditIndexerParams, IndexerSettings};
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_all_indexer_settings_command() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 1,
minimum_age: 1,
retention: 1,
rss_sync_interval: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(
IndexerSettings {
id: 1,
maximum_size: 2,
minimum_age: 2,
retention: 2,
rss_sync_interval: 2,
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = LidarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
};
let result = LidarrEditCommandHandler::with(
&app_arc,
edit_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[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);
}
#[tokio::test]
async fn test_handle_edit_indexer_command() {
let expected_edit_indexer_params = EditIndexerParams {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: Some(true),
enable_automatic_search: Some(true),
enable_interactive_search: Some(true),
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tags: Some(vec![1, 2]),
tag_input_string: None,
priority: Some(25),
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditIndexer(expected_edit_indexer_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_indexer_command = LidarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: true,
disable_rss: false,
enable_automatic_search: true,
disable_automatic_search: false,
enable_interactive_search: true,
disable_interactive_search: false,
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tag: Some(vec![1, 2]),
priority: Some(25),
clear_tags: false,
};
let result =
LidarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
+138
View File
@@ -0,0 +1,138 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "get_command_handler_tests.rs"]
mod get_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrGetCommand {
#[command(about = "Get detailed information for the album with the given ID")]
AlbumDetails {
#[arg(
long,
help = "The Lidarr ID of the album whose details you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "Get the shared settings for all indexers")]
AllIndexerSettings,
#[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,
#[command(about = "Get detailed information for the track with the given ID")]
TrackDetails {
#[arg(
long,
help = "The Lidarr ID of the track whose details you wish to fetch",
required = true
)]
track_id: i64,
},
}
impl From<LidarrGetCommand> for Command {
fn from(value: LidarrGetCommand) -> Self {
Command::Lidarr(LidarrCommand::Get(value))
}
}
pub(super) struct LidarrGetCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrGetCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrGetCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrGetCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrGetCommand::AlbumDetails { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbumDetails(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::AllIndexerSettings => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAllIndexerSettings.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::ArtistDetails { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetArtistDetails(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::HostConfig => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetHostConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::SecurityConfig => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetSecurityConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::SystemStatus => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetStatus.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::TrackDetails { track_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTrackDetails(track_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
+329
View File
@@ -0,0 +1,329 @@
#[cfg(test)]
mod tests {
use crate::Cli;
use crate::cli::{
Command,
lidarr::{LidarrCommand, get_command_handler::LidarrGetCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_get_command_from() {
let command = LidarrGetCommand::SystemStatus;
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Get(command)));
}
mod cli {
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_album_details_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "album-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"get",
"album-details",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_all_indexer_settings_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "all-indexer-settings"]);
assert_ok!(&result);
}
#[test]
fn test_artist_details_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "artist-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_artist_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"get",
"artist-details",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_host_config_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "host-config"]);
assert_ok!(&result);
}
#[test]
fn test_security_config_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "security-config"]);
assert_ok!(&result);
}
#[test]
fn test_system_status_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "system-status"]);
assert_ok!(&result);
}
#[test]
fn test_track_details_requires_track_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "track-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_track_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"get",
"track-details",
"--track-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
CliCommandHandler,
lidarr::get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler},
},
models::{Serdeable, lidarr_models::LidarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_get_album_details_command() {
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumDetails(expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_album_details_command = LidarrGetCommand::AlbumDetails { album_id: 1 };
let result =
LidarrGetCommandHandler::with(&app_arc, get_album_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_all_indexer_settings_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_all_indexer_settings_command = LidarrGetCommand::AllIndexerSettings;
let result = LidarrGetCommandHandler::with(
&app_arc,
get_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_artist_details_command() {
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetArtistDetails(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_artist_details_command = LidarrGetCommand::ArtistDetails { artist_id: 1 };
let result =
LidarrGetCommandHandler::with(&app_arc, get_artist_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_host_config_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetHostConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_host_config_command = LidarrGetCommand::HostConfig;
let result =
LidarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_security_config_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetSecurityConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_security_config_command = LidarrGetCommand::SecurityConfig;
let result =
LidarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_system_status_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = LidarrGetCommand::SystemStatus;
let result =
LidarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_track_details_command() {
let expected_track_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackDetails(expected_track_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_track_details_command = LidarrGetCommand::TrackDetails { track_id: 1 };
let result =
LidarrGetCommandHandler::with(&app_arc, get_track_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
+740
View File
@@ -0,0 +1,740 @@
#[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;
use rstest::rstest;
#[rstest]
fn test_commands_that_have_no_arg_requirements(
#[values("test-all-indexers")] subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", subcommand]);
assert_ok!(&result);
}
#[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_download_release_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--indexer-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--guid",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--guid",
"1",
"--indexer-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_toggle_artist_monitoring_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-artist-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_artist_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"toggle-artist-monitoring",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_toggle_album_monitoring_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-album-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_album_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"toggle-album-monitoring",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_search_new_artist_requires_query() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "search-new-artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_search_new_artist_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"search-new-artist",
"--query",
"test query",
]);
assert_ok!(&result);
}
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "start-task"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_start_task_task_name_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"start-task",
"--task-name",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_start_task_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"start-task",
"--task-name",
"application-update-check",
]);
assert_ok!(&result);
}
#[test]
fn test_mark_history_item_as_failed_requires_history_item_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "mark-history-item-as-failed"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_mark_history_item_as_failed_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"mark-history-item-as-failed",
"--history-item-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_test_indexer_requires_indexer_id() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "test-indexer"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_test_indexer_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"test-indexer",
"--indexer-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::lidarr::add_command_handler::LidarrAddCommand;
use crate::cli::lidarr::edit_command_handler::LidarrEditCommand;
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
use crate::models::lidarr_models::{LidarrReleaseDownloadBody, LidarrTaskName};
use crate::models::servarr_models::IndexerSettings;
use crate::{
app::App,
cli::{
CliCommandHandler,
lidarr::{
LidarrCliHandler, LidarrCommand, delete_command_handler::LidarrDeleteCommand,
list_command_handler::LidarrListCommand,
},
},
models::{
Serdeable,
lidarr_models::{Artist, DeleteParams, LidarrSerdeable},
},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = LidarrCommand::Add(LidarrAddCommand::Tag {
name: expected_tag_name,
});
let result = LidarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = LidarrCommand::Get(LidarrGetCommand::SystemStatus);
let result = LidarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
let expected_delete_artist_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteArtist(expected_delete_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_artist_command = LidarrCommand::Delete(LidarrDeleteCommand::Artist {
artist_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
});
let result = LidarrCliHandler::with(&app_arc, delete_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 1,
minimum_age: 1,
retention: 1,
rss_sync_interval: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(
IndexerSettings {
id: 1,
maximum_size: 2,
minimum_age: 2,
retention: 2,
rss_sync_interval: 2,
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command =
LidarrCommand::Edit(LidarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
});
let result = LidarrCliHandler::with(
&app_arc,
edit_all_indexer_settings_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_artist_command = LidarrCommand::Refresh(LidarrRefreshCommand::AllArtists);
let result = LidarrCliHandler::with(&app_arc, refresh_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler()
{
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command =
LidarrCommand::ManualSearch(LidarrManualSearchCommand::Discography { artist_id: 1 });
let result =
LidarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist {
artist_id: 1,
});
let result = LidarrCliHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_download_release_command() {
let expected_release_download_body = LidarrReleaseDownloadBody {
guid: "guid".to_owned(),
indexer_id: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DownloadRelease(expected_release_download_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_release_command = LidarrCommand::DownloadRelease {
guid: "guid".to_owned(),
indexer_id: 1,
};
let result = LidarrCliHandler::with(&app_arc, download_release_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_toggle_artist_monitoring_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::ToggleArtistMonitoring(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_artist_monitoring_command = LidarrCommand::ToggleArtistMonitoring { artist_id: 1 };
let result = LidarrCliHandler::with(
&app_arc,
toggle_artist_monitoring_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_search_new_artist_command() {
let expected_query = "test artist".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::SearchNewArtist(expected_query.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let search_new_artist_command = LidarrCommand::SearchNewArtist {
query: expected_query,
};
let result = LidarrCliHandler::with(&app_arc, search_new_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_start_task_command() {
let expected_task_name = LidarrTaskName::ApplicationUpdateCheck;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::StartTask(expected_task_name).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let start_task_command = LidarrCommand::StartTask {
task_name: LidarrTaskName::ApplicationUpdateCheck,
};
let result = LidarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_test_indexer_command() {
let expected_indexer_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TestIndexer(expected_indexer_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_indexer_command = LidarrCommand::TestIndexer { indexer_id: 1 };
let result = LidarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_test_all_indexers_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::TestAllIndexers.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_all_indexers_command = LidarrCommand::TestAllIndexers;
let result = LidarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_mark_history_item_as_failed_command() {
let expected_history_item_id = 1i64;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let mark_history_item_as_failed_command = LidarrCommand::MarkHistoryItemAsFailed {
history_item_id: expected_history_item_id,
};
let result = LidarrCliHandler::with(
&app_arc,
mark_history_item_as_failed_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
+332
View File
@@ -0,0 +1,332 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{Subcommand, arg};
use serde_json::json;
use tokio::sync::Mutex;
use super::LidarrCommand;
use crate::models::Serdeable;
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
#[cfg(test)]
#[path = "list_command_handler_tests.rs"]
mod list_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrListCommand {
#[command(about = "List all albums for the artist with the given ID")]
Albums {
#[arg(
long,
help = "The Lidarr ID of the artist whose albums you want to list",
required = true
)]
artist_id: i64,
},
#[command(
about = "Fetch all history events for the given album corresponding to the artist with the given ID."
)]
AlbumHistory {
#[arg(
long,
help = "The Lidarr artist ID of the artist whose history you wish to fetch and list",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The Lidarr album ID to fetch history events for",
required = true
)]
album_id: i64,
},
#[command(about = "Fetch all history events for the artist with the given ID")]
ArtistHistory {
#[arg(
long,
help = "The Lidarr ID of the artist whose history you wish to fetch",
required = true
)]
artist_id: i64,
},
#[command(about = "List all artists in your Lidarr library")]
Artists,
#[command(about = "List all active downloads in Lidarr")]
Downloads {
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
count: u64,
},
#[command(about = "Fetch all Lidarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
events: u64,
},
#[command(about = "List all Lidarr indexers")]
Indexers,
#[command(about = "Fetch Lidarr logs")]
Logs {
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
events: u64,
#[arg(
long,
help = "Output the logs in the same format as they appear in the log files"
)]
output_in_log_format: bool,
},
#[command(about = "List all Lidarr metadata profiles")]
MetadataProfiles,
#[command(about = "List all Lidarr quality profiles")]
QualityProfiles,
#[command(about = "List all queued events")]
QueuedEvents,
#[command(about = "List all root folders in Lidarr")]
RootFolders,
#[command(about = "List all Lidarr tags")]
Tags,
#[command(about = "List all Lidarr tasks")]
Tasks,
#[command(about = "Fetch all history events for the track with the given ID")]
TrackHistory {
#[arg(
long,
help = "The artist ID that the track belongs to",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The album ID that the track is a part of",
required = true
)]
album_id: i64,
#[arg(
long,
help = "The Lidarr ID of the track whose history you wish to fetch",
required = true
)]
track_id: i64,
},
#[command(
about = "List the tracks for the album that corresponds to the artist with the given ID"
)]
Tracks {
#[arg(
long,
help = "The Lidarr artist ID of the artist whose tracks you wish to fetch",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The Lidarr album ID whose tracks you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "List the track files for the album with the given ID")]
TrackFiles {
#[arg(
long,
help = "The Lidarr ID of the album whose track files you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "List all Lidarr updates")]
Updates,
}
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,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrListCommand::Albums { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbums(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::AlbumHistory {
artist_id,
album_id,
} => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbumHistory(artist_id, album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::ArtistHistory { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetArtistHistory(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Artists => {
let resp = self
.network
.handle_network_event(LidarrEvent::ListArtists.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Downloads { count } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::History { events: items } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetHistory(items).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Indexers => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Logs {
events,
output_in_log_format,
} => {
let logs = self
.network
.handle_network_event(LidarrEvent::GetLogs(events).into())
.await?;
if output_in_log_format {
let log_lines = &self.app.lock().await.data.sonarr_data.logs.items;
serde_json::to_string_pretty(log_lines)?
} else {
serde_json::to_string_pretty(&logs)?
}
}
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::QueuedEvents => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetQueuedEvents.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::RootFolders => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetRootFolders.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)?
}
LidarrListCommand::Tasks => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTasks.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::TrackHistory {
artist_id,
album_id,
track_id,
} => {
match self
.network
.handle_network_event(LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(history_vec))) => {
let history_items_vec: Vec<LidarrHistoryItem> = history_vec
.into_iter()
.filter(|it| it.track_id == track_id)
.collect();
serde_json::to_string_pretty(&history_items_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
LidarrListCommand::Tracks {
artist_id,
album_id,
} => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTracks(artist_id, album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::TrackFiles { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTrackFiles(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Updates => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetUpdates.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,724 @@
#[cfg(test)]
mod tests {
use crate::Cli;
use crate::cli::{
Command,
lidarr::{LidarrCommand, list_command_handler::LidarrListCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_list_command_from() {
let command = LidarrListCommand::Artists;
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::List(command)));
}
mod cli {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_list_commands_have_no_arg_requirements(
#[values(
"artists",
"indexers",
"metadata-profiles",
"quality-profiles",
"queued-events",
"tags",
"tasks",
"updates",
"root-folders"
)]
subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", subcommand]);
assert_ok!(&result);
}
#[test]
fn test_list_albums_requires_artist_id() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "albums"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_albums_with_artist_id() {
let expected_args = LidarrListCommand::Albums { artist_id: 1 };
let result =
Cli::try_parse_from(["managarr", "lidarr", "list", "albums", "--artist-id", "1"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(album_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(album_command, expected_args);
}
#[test]
fn test_album_history_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_history_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_history_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_list_artist_history_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artist-history"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_artist_history_success() {
let expected_args = LidarrListCommand::ArtistHistory { artist_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"artist-history",
"--artist-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(artist_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(artist_command, expected_args);
}
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "downloads", "--count"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_downloads_default_values() {
let expected_args = LidarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "downloads"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(downloads_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(downloads_command, expected_args);
}
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "history", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_history_default_values() {
let expected_args = LidarrListCommand::History { events: 500 };
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "history"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(history_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(history_command, expected_args);
}
#[test]
fn test_list_logs_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "logs", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_logs_default_values() {
let expected_args = LidarrListCommand::Logs {
events: 500,
output_in_log_format: false,
};
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "logs"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(logs_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(logs_command, expected_args);
}
#[test]
fn test_list_track_history_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--album-id",
"1",
"--track-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--track-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_requires_track_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_history_success() {
let expected_args = LidarrListCommand::TrackHistory {
artist_id: 1,
album_id: 1,
track_id: 1,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"track-history",
"--artist-id",
"1",
"--album-id",
"1",
"--track-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(track_history_command))) =
result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(track_history_command, expected_args);
}
#[test]
fn test_list_tracks_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"tracks",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_tracks_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"tracks",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_tracks_success() {
let expected_args = LidarrListCommand::Tracks {
artist_id: 1,
album_id: 1,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"tracks",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(tracks_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(tracks_command, expected_args);
}
#[test]
fn test_list_track_files_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "track-files"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_files_success() {
let expected_args = LidarrListCommand::TrackFiles { album_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"track-files",
"--album-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(track_files_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(track_files_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use pretty_assertions::assert_str_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::{LidarrHistoryItem, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::lidarr_history_item;
use crate::{
app::App,
network::{MockNetworkTrait, NetworkEvent},
};
#[rstest]
#[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)]
#[case(LidarrListCommand::Indexers, LidarrEvent::GetIndexers)]
#[case(LidarrListCommand::MetadataProfiles, LidarrEvent::GetMetadataProfiles)]
#[case(LidarrListCommand::QualityProfiles, LidarrEvent::GetQualityProfiles)]
#[case(LidarrListCommand::QueuedEvents, LidarrEvent::GetQueuedEvents)]
#[case(LidarrListCommand::RootFolders, LidarrEvent::GetRootFolders)]
#[case(LidarrListCommand::Tags, LidarrEvent::GetTags)]
#[case(LidarrListCommand::Tasks, LidarrEvent::GetTasks)]
#[case(LidarrListCommand::Updates, LidarrEvent::GetUpdates)]
#[tokio::test]
async fn test_handle_list_command(
#[case] list_command: LidarrListCommand,
#[case] expected_lidarr_event: LidarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(expected_lidarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_albums_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetAlbums(1).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_command = LidarrListCommand::Albums { artist_id: 1 };
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_list_album_history_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumHistory(expected_artist_id, expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_album_history_command = LidarrListCommand::AlbumHistory {
artist_id: 1,
album_id: 1,
};
let result =
LidarrListCommandHandler::with(&app_arc, list_album_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_artist_history_command() {
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetArtistHistory(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 list_artist_history_command = LidarrListCommand::ArtistHistory { artist_id: 1 };
let result =
LidarrListCommandHandler::with(&app_arc, list_artist_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_downloads_command() {
let expected_count = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = LidarrListCommand::Downloads { count: 1000 };
let result =
LidarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_history_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetHistory(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_history_command = LidarrListCommand::History { events: 1000 };
let result =
LidarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_logs_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetLogs(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_logs_command = LidarrListCommand::Logs {
events: 1000,
output_in_log_format: false,
};
let result = LidarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_track_history_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let expected_track_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackHistory(expected_artist_id, expected_album_id, expected_track_id)
.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(
vec![
lidarr_history_item(),
LidarrHistoryItem {
track_id: 2,
..lidarr_history_item()
},
],
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_track_history_command = LidarrListCommand::TrackHistory {
artist_id: expected_artist_id,
album_id: expected_album_id,
track_id: expected_track_id,
};
let result =
LidarrListCommandHandler::with(&app_arc, list_track_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&[lidarr_history_item()]).unwrap()
);
}
#[tokio::test]
async fn test_handle_list_tracks_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTracks(expected_artist_id, expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_tracks_command = LidarrListCommand::Tracks {
artist_id: 1,
album_id: 1,
};
let result = LidarrListCommandHandler::with(&app_arc, list_tracks_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_track_files_command() {
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackFiles(expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_track_files_command = LidarrListCommand::TrackFiles { album_id: 1 };
let result =
LidarrListCommandHandler::with(&app_arc, list_track_files_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -0,0 +1,118 @@
use crate::app::App;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::{CliCommandHandler, Command};
use crate::models::Serdeable;
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
use crate::network::NetworkTrait;
use crate::network::lidarr_network::LidarrEvent;
use anyhow::Result;
use clap::Subcommand;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::Mutex;
#[cfg(test)]
#[path = "manual_search_command_handler_tests.rs"]
mod manual_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrManualSearchCommand {
#[command(
about = "Trigger a manual search of releases for the given album corresponding to the artist with the given ID"
)]
Album {
#[arg(
long,
help = "The Lidarr ID of the artist whose releases you wish to fetch and list",
required = true
)]
artist_id: i64,
#[arg(long, help = "The Lidarr album ID to search for", required = true)]
album_id: i64,
},
#[command(
about = "Trigger a manual search of discography releases for the given artist corresponding to the artist with the given ID."
)]
Discography {
#[arg(
long,
help = "The Lidarr ID of the artist whose discography releases you wish to fetch and list",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrManualSearchCommand> for Command {
fn from(value: LidarrManualSearchCommand) -> Self {
Command::Lidarr(LidarrCommand::ManualSearch(value))
}
}
pub(super) struct LidarrManualSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrManualSearchCommand>
for LidarrManualSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrManualSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrManualSearchCommand::Album {
artist_id,
album_id,
} => {
println!("Searching for album releases. This may take a minute...");
match self
.network
.handle_network_event(LidarrEvent::GetAlbumReleases(artist_id, album_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => {
let albums_vec: Vec<LidarrRelease> = releases_vec
.into_iter()
.filter(|release| !release.discography)
.collect();
serde_json::to_string_pretty(&albums_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
LidarrManualSearchCommand::Discography { artist_id } => {
println!("Searching for artist discography releases. This may take a minute...");
match self
.network
.handle_network_event(LidarrEvent::GetDiscographyReleases(artist_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => {
let discography_vec: Vec<LidarrRelease> = releases_vec
.into_iter()
.filter(|release| release.discography)
.collect();
serde_json::to_string_pretty(&discography_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
};
Ok(result)
}
}
@@ -0,0 +1,207 @@
#[cfg(test)]
mod tests {
use crate::cli::Command;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_manual_search_command_from() {
let command = LidarrManualSearchCommand::Discography { artist_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Lidarr(LidarrCommand::ManualSearch(command))
);
}
mod cli {
use crate::Cli;
use clap::CommandFactory;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_manual_album_search_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_album_search_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_album_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_manual_discography_search_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "manual-search", "discography"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_discography_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"discography",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use crate::app::App;
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::manual_search_command_handler::{
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::Serdeable;
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
torrent_release, usenet_release,
};
use crate::network::{MockNetworkTrait, NetworkEvent};
use mockall::predicate::eq;
use pretty_assertions::assert_str_eq;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_manual_album_search_command() {
let expected_releases = [torrent_release()];
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumReleases(expected_artist_id, expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
torrent_release(),
LidarrRelease {
discography: true,
..usenet_release()
},
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_album_search_command = LidarrManualSearchCommand::Album {
artist_id: 1,
album_id: 1,
};
let result = LidarrManualSearchCommandHandler::with(
&app_arc,
manual_album_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&expected_releases).unwrap()
);
}
#[tokio::test]
async fn test_manual_discography_search_command() {
let expected_releases = [LidarrRelease {
discography: true,
..usenet_release()
}];
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
torrent_release(),
LidarrRelease {
discography: true,
..usenet_release()
},
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_discography_search_command =
LidarrManualSearchCommand::Discography { artist_id: 1 };
let result = LidarrManualSearchCommandHandler::with(
&app_arc,
manual_discography_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&expected_releases).unwrap()
);
}
}
}
+282
View File
@@ -0,0 +1,282 @@
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 serde_json::json;
use tokio::sync::Mutex;
use trigger_automatic_search_command_handler::{
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
};
use super::{CliCommandHandler, Command};
use crate::cli::lidarr::manual_search_command_handler::{
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::lidarr_models::{LidarrReleaseDownloadBody, LidarrTaskName};
use crate::network::lidarr_network::LidarrEvent;
use crate::{app::App, network::NetworkTrait};
mod add_command_handler;
mod delete_command_handler;
mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod manual_search_command_handler;
mod refresh_command_handler;
mod trigger_automatic_search_command_handler;
#[cfg(test)]
#[path = "lidarr_command_tests.rs"]
mod lidarr_command_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrCommand {
#[command(
subcommand,
about = "Commands to add or create new resources within your Lidarr instance"
)]
Add(LidarrAddCommand),
#[command(
subcommand,
about = "Commands to delete resources from your Lidarr instance"
)]
Delete(LidarrDeleteCommand),
#[command(
subcommand,
about = "Commands to edit resources in your Lidarr instance"
)]
Edit(LidarrEditCommand),
#[command(
subcommand,
about = "Commands to fetch details of the resources in your Lidarr instance"
)]
Get(LidarrGetCommand),
#[command(
subcommand,
about = "Commands to list attributes from your Lidarr instance"
)]
List(LidarrListCommand),
#[command(
subcommand,
about = "Commands to refresh the data in your Lidarr instance"
)]
Refresh(LidarrRefreshCommand),
#[command(subcommand, about = "Commands to manually search for releases")]
ManualSearch(LidarrManualSearchCommand),
#[command(
subcommand,
about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance"
)]
TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand),
#[command(about = "Manually download the given release")]
DownloadRelease {
#[arg(long, help = "The GUID of the release to download", required = true)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
},
#[command(about = "Mark the Lidarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
long,
help = "The Lidarr ID of the history item you wish to mark as 'failed'",
required = true
)]
history_item_id: i64,
},
#[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 = "Start the specified Lidarr task")]
StartTask {
#[arg(
long,
help = "The name of the task to trigger",
value_enum,
required = true
)]
task_name: LidarrTaskName,
},
#[command(
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
)]
TestIndexer {
#[arg(long, help = "The ID of the indexer to test", required = true)]
indexer_id: i64,
},
#[command(about = "Test all Lidarr indexers")]
TestAllIndexers,
#[command(
about = "Toggle monitoring for the specified album corresponding to the given album ID"
)]
ToggleAlbumMonitoring {
#[arg(
long,
help = "The Lidarr ID of the album to toggle monitoring on",
required = true
)]
album_id: i64,
},
#[command(
about = "Toggle monitoring for the specified artist corresponding to the given artist ID"
)]
ToggleArtistMonitoring {
#[arg(
long,
help = "The Lidarr ID of the artist to toggle monitoring on",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrCommand> for Command {
fn from(lidarr_command: LidarrCommand) -> Command {
Command::Lidarr(lidarr_command)
}
}
pub(super) struct LidarrCliHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrCliHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrCommand::Add(add_command) => {
LidarrAddCommandHandler::with(self.app, add_command, self.network)
.handle()
.await?
}
LidarrCommand::Delete(delete_command) => {
LidarrDeleteCommandHandler::with(self.app, delete_command, self.network)
.handle()
.await?
}
LidarrCommand::Edit(edit_command) => {
LidarrEditCommandHandler::with(self.app, edit_command, self.network)
.handle()
.await?
}
LidarrCommand::Get(get_command) => {
LidarrGetCommandHandler::with(self.app, get_command, self.network)
.handle()
.await?
}
LidarrCommand::List(list_command) => {
LidarrListCommandHandler::with(self.app, list_command, self.network)
.handle()
.await?
}
LidarrCommand::Refresh(refresh_command) => {
LidarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
.handle()
.await?
}
LidarrCommand::ManualSearch(manual_search_command) => {
LidarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network)
.handle()
.await?
}
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
LidarrTriggerAutomaticSearchCommandHandler::with(
self.app,
trigger_automatic_search_command,
self.network,
)
.handle()
.await?
}
LidarrCommand::DownloadRelease { guid, indexer_id } => {
let params = LidarrReleaseDownloadBody { guid, indexer_id };
let resp = self
.network
.handle_network_event(LidarrEvent::DownloadRelease(params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
.handle_network_event(LidarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
serde_json::to_string_pretty(&json!({"message": "Lidarr history item marked as 'failed'"}))?
}
LidarrCommand::SearchNewArtist { query } => {
let resp = self
.network
.handle_network_event(LidarrEvent::SearchNewArtist(query).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::StartTask { task_name } => {
let resp = self
.network
.handle_network_event(LidarrEvent::StartTask(task_name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::TestIndexer { indexer_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::TestIndexer(indexer_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::TestAllIndexers => {
println!("Testing all Lidarr indexers. This may take a minute...");
let resp = self
.network
.handle_network_event(LidarrEvent::TestAllIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::ToggleAlbumMonitoring { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::ToggleAlbumMonitoring(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::ToggleArtistMonitoring { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::ToggleArtistMonitoring(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
+89
View File
@@ -0,0 +1,89 @@
use std::sync::Arc;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "refresh_command_handler_tests.rs"]
mod refresh_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrRefreshCommand {
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
AllArtists,
#[command(about = "Refresh artist data and scan disk for the artist with the given ID")]
Artist {
#[arg(
long,
help = "The ID of the artist to refresh information on and to scan the disk for",
required = true
)]
artist_id: i64,
},
#[command(about = "Refresh all downloads in Lidarr")]
Downloads,
}
impl From<LidarrRefreshCommand> for Command {
fn from(value: LidarrRefreshCommand) -> Self {
Command::Lidarr(LidarrCommand::Refresh(value))
}
}
pub(super) struct LidarrRefreshCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
for LidarrRefreshCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrRefreshCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> anyhow::Result<String> {
let result = match self.command {
LidarrRefreshCommand::AllArtists => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateAllArtists.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrRefreshCommand::Artist { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateAndScanArtist(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrRefreshCommand::Downloads => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,138 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::Cli;
use crate::cli::{
Command,
lidarr::{LidarrCommand, refresh_command_handler::LidarrRefreshCommand},
};
use clap::CommandFactory;
#[test]
fn test_lidarr_refresh_command_from() {
let command = LidarrRefreshCommand::AllArtists;
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Refresh(command)));
}
mod cli {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_refresh_commands_have_no_arg_requirements(
#[values("all-artists", "downloads")] subcommand: &str,
) {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", subcommand]);
assert_ok!(&result);
}
#[test]
fn test_refresh_artist_requires_artist_id() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_refresh_artist_with_artist_id() {
let expected_args = LidarrRefreshCommand::Artist { artist_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"refresh",
"artist",
"--artist-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Refresh(refresh_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(refresh_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use rstest::rstest;
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},
};
#[rstest]
#[case(LidarrRefreshCommand::AllArtists, LidarrEvent::UpdateAllArtists)]
#[case(LidarrRefreshCommand::Downloads, LidarrEvent::UpdateDownloads)]
#[tokio::test]
async fn test_handle_refresh_command(
#[case] refresh_command: LidarrRefreshCommand,
#[case] expected_sonarr_event: LidarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(expected_sonarr_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 = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_refresh_artist_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::UpdateAndScanArtist(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = LidarrRefreshCommand::Artist { artist_id: 1 };
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -0,0 +1,88 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "trigger_automatic_search_command_handler_tests.rs"]
mod trigger_automatic_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrTriggerAutomaticSearchCommand {
#[command(about = "Trigger an automatic search for the album with the specified ID")]
Album {
#[arg(
long,
help = "The Lidarr ID of the album you want to trigger an automatic search for",
required = true
)]
album_id: i64,
},
#[command(about = "Trigger an automatic search for the artist with the specified ID")]
Artist {
#[arg(
long,
help = "The ID of the artist you want to trigger an automatic search for",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrTriggerAutomaticSearchCommand> for Command {
fn from(value: LidarrTriggerAutomaticSearchCommand) -> Self {
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(value))
}
}
pub(super) struct LidarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrTriggerAutomaticSearchCommand>
for LidarrTriggerAutomaticSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrTriggerAutomaticSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrTriggerAutomaticSearchCommand::Album { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::TriggerAutomaticAlbumSearch(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrTriggerAutomaticSearchCommand::Artist { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::TriggerAutomaticArtistSearch(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,166 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::Cli;
use crate::cli::{
Command,
lidarr::{
LidarrCommand, trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand,
},
};
use clap::CommandFactory;
#[test]
fn test_lidarr_trigger_automatic_search_command_from() {
let command = LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(command))
);
}
mod cli {
use super::*;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_trigger_automatic_album_search_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"album",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_album_search_with_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"album",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_trigger_automatic_artist_search_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"artist",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_artist_search_with_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"artist",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::lidarr::trigger_automatic_search_command_handler::{
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
};
use crate::{app::App, cli::CliCommandHandler};
use crate::{
models::{Serdeable, lidarr_models::LidarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_trigger_automatic_album_search_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticAlbumSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrTriggerAutomaticSearchCommand::Album { album_id: 1 };
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_trigger_automatic_artist_search_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
+10
View File
@@ -3,12 +3,14 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use clap::{Subcommand, command}; use clap::{Subcommand, command};
use clap_complete::Shell; use clap_complete::Shell;
use lidarr::{LidarrCliHandler, LidarrCommand};
use radarr::{RadarrCliHandler, RadarrCommand}; use radarr::{RadarrCliHandler, RadarrCommand};
use sonarr::{SonarrCliHandler, SonarrCommand}; use sonarr::{SonarrCliHandler, SonarrCommand};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{app::App, network::NetworkTrait}; use crate::{app::App, network::NetworkTrait};
pub mod lidarr;
pub mod radarr; pub mod radarr;
pub mod sonarr; pub mod sonarr;
@@ -24,6 +26,9 @@ pub enum Command {
#[command(subcommand, about = "Commands for manging your Sonarr instance")] #[command(subcommand, about = "Commands for manging your Sonarr instance")]
Sonarr(SonarrCommand), Sonarr(SonarrCommand),
#[command(subcommand, about = "Commands for manging your Lidarr instance")]
Lidarr(LidarrCommand),
#[command( #[command(
arg_required_else_help = true, arg_required_else_help = true,
about = "Generate shell completions for the Managarr CLI" about = "Generate shell completions for the Managarr CLI"
@@ -61,6 +66,11 @@ pub(crate) async fn handle_command(
.handle() .handle()
.await? .await?
} }
Command::Lidarr(lidarr_command) => {
LidarrCliHandler::with(app, lidarr_command, network)
.handle()
.await?
}
_ => String::new(), _ => String::new(),
}; };
+2 -2
View File
@@ -122,12 +122,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
title: String::new(), title: String::new(),
root_folder_path, root_folder_path,
quality_profile_id, quality_profile_id,
minimum_availability: minimum_availability.to_string(), minimum_availability,
monitored: !disable_monitoring, monitored: !disable_monitoring,
tags, tags,
tag_input_string: None, tag_input_string: None,
add_options: AddMovieOptions { add_options: AddMovieOptions {
monitor: monitor.to_string(), monitor,
search_for_movie: !no_search_for_movie, search_for_movie: !no_search_for_movie,
}, },
}; };
+2 -2
View File
@@ -384,12 +384,12 @@ mod tests {
title: String::new(), title: String::new(),
root_folder_path: "/test".to_owned(), root_folder_path: "/test".to_owned(),
quality_profile_id: 1, quality_profile_id: 1,
minimum_availability: "released".to_owned(), minimum_availability: MinimumAvailability::Released,
monitored: false, monitored: false,
tags: vec![1, 2], tags: vec![1, 2],
tag_input_string: None, tag_input_string: None,
add_options: AddMovieOptions { add_options: AddMovieOptions {
monitor: "movieAndCollection".to_owned(), monitor: MovieMonitor::MovieAndCollection,
search_for_movie: false, search_for_movie: false,
}, },
}; };
+12
View File
@@ -29,6 +29,11 @@ pub enum RadarrListCommand {
}, },
#[command(about = "List disk space details for all provisioned root folders in Radarr")] #[command(about = "List disk space details for all provisioned root folders in Radarr")]
DiskSpace, DiskSpace,
#[command(about = "Fetch all Radarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
events: u64,
},
#[command(about = "List all Radarr indexers")] #[command(about = "List all Radarr indexers")]
Indexers, Indexers,
#[command(about = "Fetch Radarr logs")] #[command(about = "Fetch Radarr logs")]
@@ -121,6 +126,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrListCommand::History { events: items } => {
let resp = self
.network
.handle_network_event(RadarrEvent::GetHistory(items).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Indexers => { RadarrListCommand::Indexers => {
let resp = self let resp = self
.network .network
@@ -111,6 +111,29 @@ mod tests {
assert_eq!(refresh_command, expected_args); assert_eq!(refresh_command, expected_args);
} }
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "history", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_history_default_values() {
let expected_args = RadarrListCommand::History { events: 500 };
let result = Cli::try_parse_from(["managarr", "radarr", "list", "history"]);
assert_ok!(&result);
let Some(Command::Radarr(RadarrCommand::List(history_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(history_command, expected_args);
}
#[test] #[test]
fn test_list_logs_default_values() { fn test_list_logs_default_values() {
let expected_args = RadarrListCommand::Logs { let expected_args = RadarrListCommand::Logs {
@@ -233,6 +256,32 @@ mod tests {
assert_ok!(&result); assert_ok!(&result);
} }
#[tokio::test]
async fn test_handle_list_history_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetHistory(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_history_command = RadarrListCommand::History { events: 1000 };
let result =
RadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test] #[tokio::test]
async fn test_handle_list_logs_command() { async fn test_handle_list_logs_command() {
let expected_events = 1000; let expected_events = 1000;
+18
View File
@@ -64,6 +64,15 @@ pub enum RadarrCommand {
Refresh(RadarrRefreshCommand), Refresh(RadarrRefreshCommand),
#[command(about = "Clear the blocklist")] #[command(about = "Clear the blocklist")]
ClearBlocklist, ClearBlocklist,
#[command(about = "Mark the Radarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
long,
help = "The Radarr ID of the history item you wish to mark as 'failed'",
required = true
)]
history_item_id: i64,
},
#[command(about = "Manually download the given release for the specified movie ID")] #[command(about = "Manually download the given release for the specified movie ID")]
DownloadRelease { DownloadRelease {
#[arg(long, help = "The GUID of the release to download", required = true)] #[arg(long, help = "The GUID of the release to download", required = true)]
@@ -208,6 +217,15 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
.handle_network_event(RadarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
serde_json::to_string_pretty(
&serde_json::json!({"message": "Radarr history item marked as 'failed'"}),
)?
}
RadarrCommand::DownloadRelease { RadarrCommand::DownloadRelease {
guid, guid,
indexer_id, indexer_id,
+55
View File
@@ -31,6 +31,31 @@ mod tests {
assert_ok!(&result); assert_ok!(&result);
} }
#[test]
fn test_mark_history_item_as_failed_requires_history_item_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "mark-history-item-as-failed"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_mark_history_item_as_failed_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"mark-history-item-as-failed",
"--history-item-id",
"1",
]);
assert_ok!(&result);
}
#[test] #[test]
fn test_download_release_requires_movie_id() { fn test_download_release_requires_movie_id() {
let result = Cli::command().try_get_matches_from([ let result = Cli::command().try_get_matches_from([
@@ -327,6 +352,36 @@ mod tests {
assert_ok!(&result); assert_ok!(&result);
} }
#[tokio::test]
async fn test_mark_history_item_as_failed_command() {
let expected_history_item_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let mark_history_item_as_failed_command =
RadarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 };
let result = RadarrCliHandler::with(
&app_arc,
mark_history_item_as_failed_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test] #[tokio::test]
async fn test_download_release_command() { async fn test_download_release_command() {
let expected_release_download_body = RadarrReleaseDownloadBody { let expected_release_download_body = RadarrReleaseDownloadBody {
+2 -2
View File
@@ -137,12 +137,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan
root_folder_path, root_folder_path,
quality_profile_id, quality_profile_id,
language_profile_id, language_profile_id,
series_type: series_type.to_string(), series_type,
season_folder: !disable_season_folders, season_folder: !disable_season_folders,
tags, tags,
tag_input_string: None, tag_input_string: None,
add_options: AddSeriesOptions { add_options: AddSeriesOptions {
monitor: monitor.to_string(), monitor,
search_for_cutoff_unmet_episodes: !no_search_for_series, search_for_cutoff_unmet_episodes: !no_search_for_series,
search_for_missing_episodes: !no_search_for_series, search_for_missing_episodes: !no_search_for_series,
}, },
+2 -2
View File
@@ -517,13 +517,13 @@ mod tests {
root_folder_path: "/test".to_owned(), root_folder_path: "/test".to_owned(),
quality_profile_id: 1, quality_profile_id: 1,
language_profile_id: 1, language_profile_id: 1,
series_type: "anime".to_owned(), series_type: SeriesType::Anime,
monitored: false, monitored: false,
tags: vec![1, 2], tags: vec![1, 2],
tag_input_string: None, tag_input_string: None,
season_folder: false, season_folder: false,
add_options: AddSeriesOptions { add_options: AddSeriesOptions {
monitor: "future".to_owned(), monitor: SeriesMonitor::Future,
search_for_cutoff_unmet_episodes: false, search_for_cutoff_unmet_episodes: false,
search_for_missing_episodes: false, search_for_missing_episodes: false,
}, },
+2 -2
View File
@@ -9,8 +9,8 @@ use crate::{
cli::{CliCommandHandler, Command, mutex_flags_or_option}, cli::{CliCommandHandler, Command, mutex_flags_or_option},
models::{ models::{
Serdeable, Serdeable,
servarr_models::EditIndexerParams, servarr_models::{EditIndexerParams, IndexerSettings},
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable}, sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable},
}, },
network::{NetworkTrait, sonarr_network::SonarrEvent}, network::{NetworkTrait, sonarr_network::SonarrEvent},
}; };
+2 -2
View File
@@ -622,8 +622,8 @@ mod tests {
}, },
models::{ models::{
Serdeable, Serdeable,
servarr_models::EditIndexerParams, servarr_models::{EditIndexerParams, IndexerSettings},
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable}, sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable},
}, },
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent}, network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
}; };
@@ -2,16 +2,18 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use clap::Subcommand; use clap::Subcommand;
use serde_json::json;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::SonarrCommand;
use crate::models::Serdeable;
use crate::models::sonarr_models::{SonarrRelease, SonarrSerdeable};
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
network::{NetworkTrait, sonarr_network::SonarrEvent}, network::{NetworkTrait, sonarr_network::SonarrEvent},
}; };
use super::SonarrCommand;
#[cfg(test)] #[cfg(test)]
#[path = "manual_search_command_handler_tests.rs"] #[path = "manual_search_command_handler_tests.rs"]
mod manual_search_command_handler_tests; mod manual_search_command_handler_tests;
@@ -28,7 +30,7 @@ pub enum SonarrManualSearchCommand {
episode_id: i64, episode_id: i64,
}, },
#[command( #[command(
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID.\nNote that when downloading a season release, ensure that the release includes 'fullSeason: true', otherwise you'll run into issues" about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID"
)] )]
Season { Season {
#[arg( #[arg(
@@ -73,22 +75,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
let result = match self.command { let result = match self.command {
SonarrManualSearchCommand::Episode { episode_id } => { SonarrManualSearchCommand::Episode { episode_id } => {
println!("Searching for episode releases. This may take a minute..."); println!("Searching for episode releases. This may take a minute...");
let resp = self match self
.network .network
.handle_network_event(SonarrEvent::GetEpisodeReleases(episode_id).into()) .handle_network_event(SonarrEvent::GetEpisodeReleases(episode_id).into())
.await?; .await
serde_json::to_string_pretty(&resp)? {
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => {
let seasons_vec: Vec<SonarrRelease> = releases_vec
.into_iter()
.filter(|release| !release.full_season)
.collect();
serde_json::to_string_pretty(&seasons_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
} }
SonarrManualSearchCommand::Season { SonarrManualSearchCommand::Season {
series_id, series_id,
season_number, season_number,
} => { } => {
println!("Searching for season releases. This may take a minute..."); println!("Searching for season releases. This may take a minute...");
let resp = self match self
.network .network
.handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into()) .handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into())
.await?; .await
serde_json::to_string_pretty(&resp)? {
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => {
let seasons_vec: Vec<SonarrRelease> = releases_vec
.into_iter()
.filter(|release| release.full_season)
.collect();
serde_json::to_string_pretty(&seasons_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
} }
}; };
@@ -108,9 +108,13 @@ mod tests {
use std::sync::Arc; use std::sync::Arc;
use mockall::predicate::eq; use mockall::predicate::eq;
use serde_json::json; use pretty_assertions::assert_str_eq;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::models::sonarr_models::SonarrRelease;
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
torrent_release, usenet_release,
};
use crate::{ use crate::{
app::App, app::App,
cli::{ cli::{
@@ -134,9 +138,13 @@ mod tests {
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value( Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![
json!({"testResponse": "response"}), torrent_release(),
))) SonarrRelease {
full_season: true,
..usenet_release()
},
])))
}); });
let app_arc = Arc::new(Mutex::new(App::test_default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 }; let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 };
@@ -150,10 +158,18 @@ mod tests {
.await; .await;
assert_ok!(&result); assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&[torrent_release()]).unwrap()
);
} }
#[tokio::test] #[tokio::test]
async fn test_manual_season_search_command() { async fn test_manual_season_search_command() {
let expected_release = SonarrRelease {
full_season: true,
..usenet_release()
};
let expected_series_id = 1; let expected_series_id = 1;
let expected_season_number = 1; let expected_season_number = 1;
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
@@ -164,9 +180,13 @@ mod tests {
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value( Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![
json!({"testResponse": "response"}), torrent_release(),
))) SonarrRelease {
full_season: true,
..usenet_release()
},
])))
}); });
let app_arc = Arc::new(Mutex::new(App::test_default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_season_search_command = SonarrManualSearchCommand::Season { let manual_season_search_command = SonarrManualSearchCommand::Season {
@@ -183,6 +203,10 @@ mod tests {
.await; .await;
assert_ok!(&result); assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&[expected_release]).unwrap()
);
} }
} }
} }
+2 -1
View File
@@ -10,6 +10,7 @@ use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler};
use list_command_handler::{SonarrListCommand, SonarrListCommandHandler}; use list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler}; use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler};
use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler}; use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler};
use serde_json::json;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use trigger_automatic_search_command_handler::{ use trigger_automatic_search_command_handler::{
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler, SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
@@ -251,7 +252,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
.network .network
.handle_network_event(SonarrEvent::MarkHistoryItemAsFailed(history_item_id).into()) .handle_network_event(SonarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?; .await?;
"Sonarr history item marked as 'failed'".to_owned() serde_json::to_string_pretty(&json!({"message": "Sonarr history item marked as 'failed'"}))?
} }
SonarrCommand::SearchNewSeries { query } => { SonarrCommand::SearchNewSeries { query } => {
let resp = self let resp = self
+3 -2
View File
@@ -266,9 +266,10 @@ mod tests {
}, },
models::{ models::{
Serdeable, Serdeable,
servarr_models::IndexerSettings,
sonarr_models::{ sonarr_models::{
BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody, BlocklistItem, BlocklistResponse, Series, SonarrReleaseDownloadBody, SonarrSerdeable,
SonarrSerdeable, SonarrTaskName, SonarrTaskName,
}, },
}, },
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent}, network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
+6 -2
View File
@@ -1,3 +1,4 @@
use anyhow::Result;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
use std::thread; use std::thread;
@@ -49,7 +50,10 @@ impl Events {
Events { rx } Events { rx }
} }
pub fn next(&self) -> Result<InputEvent<Key>, mpsc::RecvError> { pub fn next(&self) -> Result<Option<InputEvent<Key>>> {
self.rx.recv() match self.rx.try_recv() {
Ok(event) => Ok(Some(event)),
_ => Ok(None),
}
} }
} }
+2 -31
View File
@@ -4,13 +4,12 @@ mod property_tests {
use crate::app::App; use crate::app::App;
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*; use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
use crate::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use crate::models::radarr_models::Movie; use crate::models::{Paginated, Scrollable};
use crate::models::{Scrollable, Paginated};
proptest! { proptest! {
/// Property test: Table never panics on index selection
#[test] #[test]
fn test_table_index_selection_safety( fn test_table_index_selection_safety(
list_size in list_size(), list_size in list_size(),
@@ -25,19 +24,15 @@ mod property_tests {
table.set_items(movies); table.set_items(movies);
// Try to select an arbitrary index
if index < list_size { if index < list_size {
table.select_index(Some(index)); table.select_index(Some(index));
let selected = table.current_selection(); let selected = table.current_selection();
prop_assert_eq!(selected.id, index as i64); prop_assert_eq!(selected.id, index as i64);
} else { } else {
// Out of bounds selection should be safe
table.select_index(Some(index)); table.select_index(Some(index));
// Should not panic, selection stays valid
} }
} }
/// Property test: Table state remains consistent after scroll operations
#[test] #[test]
fn test_table_scroll_consistency( fn test_table_scroll_consistency(
list_size in list_size(), list_size in list_size(),
@@ -53,42 +48,34 @@ mod property_tests {
table.set_items(movies); table.set_items(movies);
let initial_id = table.current_selection().id; let initial_id = table.current_selection().id;
// Scroll down multiple times
for _ in 0..scroll_amount { for _ in 0..scroll_amount {
table.scroll_down(); table.scroll_down();
} }
let after_down_id = table.current_selection().id; let after_down_id = table.current_selection().id;
// Position should increase (up to max)
prop_assert!(after_down_id >= initial_id); prop_assert!(after_down_id >= initial_id);
prop_assert!(after_down_id < list_size as i64); prop_assert!(after_down_id < list_size as i64);
// Scroll back up
for _ in 0..scroll_amount { for _ in 0..scroll_amount {
table.scroll_up(); table.scroll_up();
} }
// Should return to initial position (or 0 if we hit the top)
prop_assert!(table.current_selection().id <= initial_id); prop_assert!(table.current_selection().id <= initial_id);
} }
/// Property test: Empty tables handle operations gracefully
#[test] #[test]
fn test_empty_table_safety(_scroll_ops in 0usize..50) { fn test_empty_table_safety(_scroll_ops in 0usize..50) {
let table = StatefulTable::<Movie>::default(); let table = StatefulTable::<Movie>::default();
// Empty table operations should be safe
prop_assert!(table.is_empty()); prop_assert!(table.is_empty());
prop_assert!(table.items.is_empty()); prop_assert!(table.items.is_empty());
} }
/// Property test: Navigation operations maintain consistency
#[test] #[test]
fn test_navigation_consistency(pushes in 1usize..20) { fn test_navigation_consistency(pushes in 1usize..20) {
let mut app = App::test_default(); let mut app = App::test_default();
let initial_route = app.get_current_route(); let initial_route = app.get_current_route();
// Push multiple routes
let routes = vec![ let routes = vec![
ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies,
ActiveRadarrBlock::Collections, ActiveRadarrBlock::Collections,
@@ -101,34 +88,27 @@ mod property_tests {
app.push_navigation_stack(route.into()); app.push_navigation_stack(route.into());
} }
// Current route should be the last pushed
let last_pushed = routes[(pushes - 1) % routes.len()]; let last_pushed = routes[(pushes - 1) % routes.len()];
prop_assert_eq!(app.get_current_route(), last_pushed.into()); prop_assert_eq!(app.get_current_route(), last_pushed.into());
// Pop all routes
for _ in 0..pushes { for _ in 0..pushes {
app.pop_navigation_stack(); app.pop_navigation_stack();
} }
// Should return to initial route
prop_assert_eq!(app.get_current_route(), initial_route); prop_assert_eq!(app.get_current_route(), initial_route);
} }
/// Property test: String input handling is safe
#[test] #[test]
fn test_string_input_safety(input in text_input_string()) { fn test_string_input_safety(input in text_input_string()) {
// String operations should never panic
let _lowercase = input.to_lowercase(); let _lowercase = input.to_lowercase();
let _uppercase = input.to_uppercase(); let _uppercase = input.to_uppercase();
let _trimmed = input.trim(); let _trimmed = input.trim();
let _len = input.len(); let _len = input.len();
let _chars: Vec<char> = input.chars().collect(); let _chars: Vec<char> = input.chars().collect();
// All operations completed without panic
prop_assert!(true); prop_assert!(true);
} }
/// Property test: Table maintains data integrity after operations
#[test] #[test]
fn test_table_data_integrity( fn test_table_data_integrity(
list_size in 1usize..100 list_size in 1usize..100
@@ -144,16 +124,13 @@ mod property_tests {
table.set_items(movies.clone()); table.set_items(movies.clone());
let original_count = table.items.len(); let original_count = table.items.len();
// Count should remain the same after various operations
prop_assert_eq!(table.items.len(), original_count); prop_assert_eq!(table.items.len(), original_count);
// All original items should still be present
for movie in &movies { for movie in &movies {
prop_assert!(table.items.iter().any(|m| m.id == movie.id)); prop_assert!(table.items.iter().any(|m| m.id == movie.id));
} }
} }
/// Property test: Page up/down maintains bounds
#[test] #[test]
fn test_page_navigation_bounds( fn test_page_navigation_bounds(
list_size in list_size(), list_size in list_size(),
@@ -168,7 +145,6 @@ mod property_tests {
table.set_items(movies); table.set_items(movies);
// Perform page operations
for i in 0..page_ops { for i in 0..page_ops {
if i % 2 == 0 { if i % 2 == 0 {
table.page_down(); table.page_down();
@@ -176,14 +152,12 @@ mod property_tests {
table.page_up(); table.page_up();
} }
// Should never exceed bounds
let current = table.current_selection(); let current = table.current_selection();
prop_assert!(current.id >= 0); prop_assert!(current.id >= 0);
prop_assert!(current.id < list_size as i64); prop_assert!(current.id < list_size as i64);
} }
} }
/// Property test: Table filtering reduces or maintains size
#[test] #[test]
fn test_table_filter_size_invariant( fn test_table_filter_size_invariant(
list_size in list_size(), list_size in list_size(),
@@ -200,7 +174,6 @@ mod property_tests {
table.set_items(movies.clone()); table.set_items(movies.clone());
let original_size = table.items.len(); let original_size = table.items.len();
// Apply filter
if !filter_term.is_empty() { if !filter_term.is_empty() {
let filtered: Vec<Movie> = movies.into_iter() let filtered: Vec<Movie> = movies.into_iter()
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase())) .filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
@@ -208,10 +181,8 @@ mod property_tests {
table.set_items(filtered); table.set_items(filtered);
} }
// Filtered size should be <= original
prop_assert!(table.items.len() <= original_size); prop_assert!(table.items.len() <= original_size);
// Selection should still be valid if table not empty
if !table.items.is_empty() { if !table.items.is_empty() {
let current = table.current_selection(); let current = table.current_selection();
prop_assert!(current.id >= 0); prop_assert!(current.id >= 0);
+1 -84
View File
@@ -330,90 +330,7 @@ mod test_utils {
#[macro_export] #[macro_export]
macro_rules! test_handler_delegation { macro_rules! test_handler_delegation {
($handler:ident, $base:expr, $active_block:expr) => { ($handler:ident, $base:expr, $active_block:expr) => {
let mut app = App::test_default(); let mut app = App::test_default_fully_populated();
app.data.sonarr_data.history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app
.data
.sonarr_data
.root_folders
.set_items(vec![$crate::models::servarr_models::RootFolder::default()]);
app
.data
.sonarr_data
.indexers
.set_items(vec![$crate::models::servarr_models::Indexer::default()]);
app
.data
.sonarr_data
.blocklist
.set_items(vec![$crate::models::sonarr_models::BlocklistItem::default()]);
app.data.sonarr_data.add_searched_series =
Some($crate::models::stateful_table::StatefulTable::default());
app
.data
.radarr_data
.movies
.set_items(vec![$crate::models::radarr_models::Movie::default()]);
app
.data
.radarr_data
.collections
.set_items(vec![$crate::models::radarr_models::Collection::default()]);
app.data.radarr_data.collection_movies.set_items(vec![
$crate::models::radarr_models::CollectionMovie::default(),
]);
app
.data
.radarr_data
.indexers
.set_items(vec![$crate::models::servarr_models::Indexer::default()]);
app
.data
.radarr_data
.root_folders
.set_items(vec![$crate::models::servarr_models::RootFolder::default()]);
app
.data
.radarr_data
.blocklist
.set_items(vec![$crate::models::radarr_models::BlocklistItem::default()]);
app.data.radarr_data.add_searched_movies =
Some($crate::models::stateful_table::StatefulTable::default());
let mut movie_details_modal =
$crate::models::servarr_data::radarr::modals::MovieDetailsModal::default();
movie_details_modal.movie_history.set_items(vec![
$crate::models::radarr_models::MovieHistoryItem::default(),
]);
movie_details_modal
.movie_cast
.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal
.movie_crew
.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal
.movie_releases
.set_items(vec![$crate::models::radarr_models::RadarrRelease::default()]);
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
let mut season_details_modal =
$crate::models::servarr_data::sonarr::modals::SeasonDetailsModal::default();
season_details_modal.season_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
season_details_modal.episode_details_modal =
Some($crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal::default());
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
let mut series_history = $crate::models::stateful_table::StatefulTable::default();
series_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app.data.sonarr_data.series_history = Some(series_history);
app
.data
.sonarr_data
.series
.set_items(vec![$crate::models::sonarr_models::Series::default()]);
app.push_navigation_stack($base.into()); app.push_navigation_stack($base.into());
app.push_navigation_stack($active_block.into()); app.push_navigation_stack($active_block.into());
+10 -4
View File
@@ -21,6 +21,7 @@ mod tests {
use crate::models::HorizontallyScrollableText; use crate::models::HorizontallyScrollableText;
use crate::models::Route; use crate::models::Route;
use crate::models::servarr_data::ActiveKeybindingBlock; use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::servarr_models::KeybindingItem; use crate::models::servarr_models::KeybindingItem;
@@ -60,11 +61,16 @@ mod tests {
} }
#[rstest] #[rstest]
#[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)] #[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)] #[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)]
fn test_handle_change_tabs<T>(#[case] index: usize, #[case] left_block: T, #[case] right_block: T) #[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)]
where fn test_handle_change_tabs<T, U>(
#[case] index: usize,
#[case] left_block: T,
#[case] right_block: U,
) where
T: Into<Route> + Copy, T: Into<Route> + Copy,
U: Into<Route> + Copy,
{ {
let mut app = App::test_default(); let mut app = App::test_default();
app.error = "Test".into(); app.error = "Test".into();
+2 -1
View File
@@ -2,6 +2,7 @@ use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::models::Route;
use crate::models::servarr_data::ActiveKeybindingBlock; use crate::models::servarr_data::ActiveKeybindingBlock;
#[cfg(test)] #[cfg(test)]
@@ -75,7 +76,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandle
self.app self.app
} }
fn current_route(&self) -> crate::models::Route { fn current_route(&self) -> Route {
self.app.get_current_route() self.app.get_current_route()
} }
} }
@@ -0,0 +1,481 @@
#[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::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::downloads::DownloadsHandler;
use crate::models::lidarr_models::DownloadRecord;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
mod test_handle_delete {
use pretty_assertions::assert_eq;
use super::*;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
#[test]
fn test_delete_download_prompt() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteDownloadPrompt.into());
}
#[test]
fn test_delete_download_prompt_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
}
}
mod test_handle_left_right_action {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Artists.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into());
}
#[rstest]
fn test_downloads_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::History.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::History.into());
}
#[rstest]
fn test_downloads_left_right_prompt_toggle(
#[values(
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use rstest::rstest;
use crate::network::lidarr_network::LidarrEvent;
use super::*;
use crate::assert_navigation_popped;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[rstest]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
LidarrEvent::DeleteDownload(1)
)]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::UpdateDownloadsPrompt,
LidarrEvent::UpdateDownloads
)]
fn test_downloads_prompt_confirm_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&expected_action
);
assert_navigation_popped!(app, base_route.into());
}
#[rstest]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)]
fn test_downloads_prompt_decline_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert_navigation_popped!(app, base_route.into());
}
}
mod test_handle_esc {
use rstest::rstest;
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)]
fn test_downloads_prompt_blocks_esc(
#[case] base_block: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(base_block.into());
app.push_navigation_stack(prompt_block.into());
app.data.lidarr_data.prompt_confirm = true;
DownloadsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
assert_navigation_popped!(app, base_block.into());
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[rstest]
fn test_default_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Downloads.into());
assert_is_empty!(app.error.text);
}
}
mod test_handle_key_char {
use super::*;
use crate::assert_navigation_popped;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_update_downloads_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateDownloadsPrompt.into());
}
#[test]
fn test_update_downloads_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
}
#[test]
fn test_refresh_downloads_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into());
assert!(app.should_refresh);
}
#[test]
fn test_refresh_downloads_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
assert!(!app.should_refresh);
}
#[rstest]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
LidarrEvent::DeleteDownload(1)
)]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::UpdateDownloadsPrompt,
LidarrEvent::UpdateDownloads
)]
fn test_downloads_prompt_confirm_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
prompt_block,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&expected_action
);
assert_navigation_popped!(app, base_route.into());
}
}
#[test]
fn test_downloads_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) {
assert!(DownloadsHandler::accepts(active_lidarr_block));
} else {
assert!(!DownloadsHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_downloads_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 = DownloadsHandler::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_extract_download_id() {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
let download_id = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.extract_download_id();
assert_eq!(download_id, 1);
}
#[test]
fn test_downloads_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = true;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_downloads_handler_not_ready_when_downloads_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = false;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = false;
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,171 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_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::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::network::lidarr_network::LidarrEvent;
#[cfg(test)]
#[path = "downloads_handler_tests.rs"]
mod downloads_handler_tests;
pub(super) struct DownloadsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl DownloadsHandler<'_, '_> {
fn extract_download_id(&self) -> i64 {
self.app.data.lidarr_data.downloads.current_selection().id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DownloadsHandler<'a, 'b> {
fn handle(&mut self) {
let download_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::Downloads.into());
if !handle_table(
self,
|app| &mut app.data.lidarr_data.downloads,
download_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
DOWNLOADS_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>,
) -> DownloadsHandler<'a, 'b> {
DownloadsHandler {
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.downloads.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::Downloads {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteDownloadPrompt.into())
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::Downloads => handle_change_tab_left_right_keys(self.app, self.key),
ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => {
handle_prompt_toggle(self.app, self.key)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteDownloadPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteDownload(self.extract_download_id()));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::UpdateDownloadsPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads);
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => {
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::Downloads => match self.key {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::UpdateDownloadsPrompt.into());
}
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveLidarrBlock::DeleteDownloadPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteDownload(self.extract_download_id()));
self.app.pop_navigation_stack();
}
}
ActiveLidarrBlock::UpdateDownloadsPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads);
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,397 @@
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::history::{HistoryHandler, history_sorting_options};
use crate::models::lidarr_models::{LidarrHistoryEventType, LidarrHistoryItem};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, HISTORY_BLOCKS};
use crate::models::servarr_models::{Quality, QualityWrapper};
mod test_handle_left_right_action {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_history_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(2);
HistoryHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::History,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Downloads.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into());
}
#[rstest]
fn test_history_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(2);
HistoryHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::History,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::RootFolders.into()
);
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::RootFolders.into()
);
}
}
mod test_handle_submit {
use pretty_assertions::assert_eq;
use super::*;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_history_submit() {
let mut app = App::test_default();
app.data.lidarr_data.history.set_items(history_vec());
app.push_navigation_stack(ActiveLidarrBlock::History.into());
HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::History, None).handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::HistoryItemDetails.into());
}
#[test]
fn test_history_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.data.lidarr_data.history.set_items(history_vec());
app.push_navigation_stack(ActiveLidarrBlock::History.into());
HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::History, None).handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into());
}
}
mod test_handle_esc {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_esc_history_item_details() {
let mut app = App::test_default();
app
.data
.lidarr_data
.history
.set_items(vec![LidarrHistoryItem::default()]);
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.push_navigation_stack(ActiveLidarrBlock::HistoryItemDetails.into());
HistoryHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::HistoryItemDetails,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::History.into());
}
#[rstest]
fn test_default_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app
.data
.lidarr_data
.history
.set_items(vec![LidarrHistoryItem::default()]);
HistoryHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::History, None).handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into());
assert_is_empty!(app.error.text);
}
}
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use super::*;
use crate::assert_navigation_pushed;
#[test]
fn test_refresh_history_key() {
let mut app = App::test_default();
app.data.lidarr_data.history.set_items(history_vec());
app.push_navigation_stack(ActiveLidarrBlock::History.into());
HistoryHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::History,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::History.into());
assert!(app.should_refresh);
}
#[test]
fn test_refresh_history_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.data.lidarr_data.history.set_items(history_vec());
app.push_navigation_stack(ActiveLidarrBlock::History.into());
HistoryHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::History,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into());
assert!(!app.should_refresh);
}
}
#[test]
fn test_history_sorting_options_source_title() {
let expected_cmp_fn: fn(&LidarrHistoryItem, &LidarrHistoryItem) -> Ordering = |a, b| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[0].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Source Title");
}
#[test]
fn test_history_sorting_options_event_type() {
let expected_cmp_fn: fn(&LidarrHistoryItem, &LidarrHistoryItem) -> Ordering = |a, b| {
a.event_type
.to_string()
.to_lowercase()
.cmp(&b.event_type.to_string().to_lowercase())
};
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[1].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Event Type");
}
#[test]
fn test_history_sorting_options_quality() {
let expected_cmp_fn: fn(&LidarrHistoryItem, &LidarrHistoryItem) -> Ordering = |a, b| {
a.quality
.quality
.name
.to_lowercase()
.cmp(&b.quality.quality.name.to_lowercase())
};
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[2].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Quality");
}
#[test]
fn test_history_sorting_options_date() {
let expected_cmp_fn: fn(&LidarrHistoryItem, &LidarrHistoryItem) -> Ordering =
|a, b| a.date.cmp(&b.date);
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[3].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Date");
}
#[test]
fn test_history_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if HISTORY_BLOCKS.contains(&active_lidarr_block) {
assert!(HistoryHandler::accepts(active_lidarr_block));
} else {
assert!(!HistoryHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_history_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 = HistoryHandler::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_history_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = true;
let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::History,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_history_handler_not_ready_when_history_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = false;
let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::History,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_history_handler_ready_when_not_loading_and_history_is_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = false;
app
.data
.lidarr_data
.history
.set_items(vec![LidarrHistoryItem::default()]);
let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::History,
None,
);
assert!(handler.is_ready());
}
fn history_vec() -> Vec<LidarrHistoryItem> {
vec![
LidarrHistoryItem {
id: 3,
source_title: "test 1".into(),
event_type: LidarrHistoryEventType::Grabbed,
quality: QualityWrapper {
quality: Quality {
name: "FLAC".to_owned(),
},
},
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
..LidarrHistoryItem::default()
},
LidarrHistoryItem {
id: 2,
source_title: "test 2".into(),
event_type: LidarrHistoryEventType::DownloadImported,
quality: QualityWrapper {
quality: Quality {
name: "MP3-320".to_owned(),
},
},
date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
..LidarrHistoryItem::default()
},
LidarrHistoryItem {
id: 1,
source_title: "test 3".into(),
event_type: LidarrHistoryEventType::TrackFileDeleted,
quality: QualityWrapper {
quality: Quality {
name: "FLAC".to_owned(),
},
},
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
..LidarrHistoryItem::default()
},
]
}
}
+165
View File
@@ -0,0 +1,165 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_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::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, HISTORY_BLOCKS};
use crate::models::stateful_table::SortOption;
#[cfg(test)]
#[path = "history_handler_tests.rs"]
mod history_handler_tests;
pub(super) struct HistoryHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl HistoryHandler<'_, '_> {}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for HistoryHandler<'a, 'b> {
fn handle(&mut self) {
let history_table_handling_config = TableHandlingConfig::new(ActiveLidarrBlock::History.into())
.sorting_block(ActiveLidarrBlock::HistorySortPrompt.into())
.sort_options(history_sorting_options())
.searching_block(ActiveLidarrBlock::SearchHistory.into())
.search_error_block(ActiveLidarrBlock::SearchHistoryError.into())
.search_field_fn(|history| &history.source_title.text)
.filtering_block(ActiveLidarrBlock::FilterHistory.into())
.filter_error_block(ActiveLidarrBlock::FilterHistoryError.into())
.filter_field_fn(|history| &history.source_title.text);
if !handle_table(
self,
|app| &mut app.data.lidarr_data.history,
history_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
HISTORY_BLOCKS.contains(&active_block)
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> Self {
HistoryHandler {
key,
app,
active_lidarr_block: active_block,
_context: context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
!self.app.is_loading && !self.app.data.lidarr_data.history.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) {}
fn handle_left_right_action(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::History {
handle_change_tab_left_right_keys(self.app, self.key)
}
}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::History {
self
.app
.push_navigation_stack(ActiveLidarrBlock::HistoryItemDetails.into());
}
}
fn handle_esc(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::HistoryItemDetails {
self.app.pop_navigation_stack();
} else {
handle_clear_errors(self.app);
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_lidarr_block == ActiveLidarrBlock::History {
match self.key {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
}
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
pub(in crate::handlers::lidarr_handlers) fn history_sorting_options()
-> Vec<SortOption<LidarrHistoryItem>> {
vec![
SortOption {
name: "Source Title",
cmp_fn: Some(|a, b| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
}),
},
SortOption {
name: "Event Type",
cmp_fn: Some(|a, b| {
a.event_type
.to_string()
.to_lowercase()
.cmp(&b.event_type.to_string().to_lowercase())
}),
},
SortOption {
name: "Quality",
cmp_fn: Some(|a, b| {
a.quality
.quality
.name
.to_lowercase()
.cmp(&b.quality.quality.name.to_lowercase())
}),
},
SortOption {
name: "Date",
cmp_fn: Some(|a, b| a.date.cmp(&b.date)),
},
]
}
@@ -0,0 +1,533 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::EditIndexerParams;
use crate::network::lidarr_network::LidarrEvent;
use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"]
mod edit_indexer_handler_tests;
pub(super) struct EditIndexerHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl EditIndexerHandler<'_, '_> {
fn build_edit_indexer_params(&mut self) -> EditIndexerParams {
let edit_indexer_modal = self
.app
.data
.lidarr_data
.edit_indexer_modal
.take()
.expect("EditIndexerModal is None");
let indexer_id = self.app.data.lidarr_data.indexers.current_selection().id;
let tags = edit_indexer_modal.tags.text;
let EditIndexerModal {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
priority,
..
} = edit_indexer_modal;
EditIndexerParams {
indexer_id,
name: Some(name.text),
enable_rss,
enable_automatic_search,
enable_interactive_search,
url: Some(url.text),
api_key: Some(api_key.text),
seed_ratio: Some(seed_ratio.text),
tags: None,
tag_input_string: Some(tags),
priority: Some(priority),
clear_tags: false,
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditIndexerHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
EDIT_INDEXER_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>,
) -> EditIndexerHandler<'a, 'b> {
EditIndexerHandler {
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_indexer_modal.is_some()
}
fn handle_scroll_up(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditIndexerPrompt => {
self.app.data.lidarr_data.selected_block.up();
}
ActiveLidarrBlock::EditIndexerPriorityInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.priority += 1;
}
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditIndexerPrompt => {
self.app.data.lidarr_data.selected_block.down();
}
ActiveLidarrBlock::EditIndexerPriorityInput => {
let edit_indexer_modal = self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap();
if edit_indexer_modal.priority > 1 {
edit_indexer_modal.priority -= 1;
}
}
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditIndexerNameInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.name
.scroll_home();
}
ActiveLidarrBlock::EditIndexerUrlInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.url
.scroll_home();
}
ActiveLidarrBlock::EditIndexerApiKeyInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.api_key
.scroll_home();
}
ActiveLidarrBlock::EditIndexerSeedRatioInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.seed_ratio
.scroll_home();
}
ActiveLidarrBlock::EditIndexerTagsInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.tags
.scroll_home();
}
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditIndexerNameInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.name
.reset_offset();
}
ActiveLidarrBlock::EditIndexerUrlInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.url
.reset_offset();
}
ActiveLidarrBlock::EditIndexerApiKeyInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.api_key
.reset_offset();
}
ActiveLidarrBlock::EditIndexerSeedRatioInput => {
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.seed_ratio
.reset_offset();
}
ActiveLidarrBlock::EditIndexerTagsInput => {
self
.app
.data
.lidarr_data
.edit_indexer_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::EditIndexerPrompt => {
handle_prompt_left_right_keys!(
self,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
lidarr_data
);
}
ActiveLidarrBlock::EditIndexerNameInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.name
);
}
ActiveLidarrBlock::EditIndexerUrlInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.url
);
}
ActiveLidarrBlock::EditIndexerApiKeyInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.api_key
);
}
ActiveLidarrBlock::EditIndexerSeedRatioInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.seed_ratio
);
}
ActiveLidarrBlock::EditIndexerTagsInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.tags
);
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditIndexerPrompt => {
let selected_block = self.app.data.lidarr_data.selected_block.get_active_block();
match selected_block {
ActiveLidarrBlock::EditIndexerConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::EditIndexer(self.build_edit_indexer_params()));
self.app.should_refresh = true;
} else {
self.app.data.lidarr_data.edit_indexer_modal = None;
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::EditIndexerNameInput
| ActiveLidarrBlock::EditIndexerUrlInput
| ActiveLidarrBlock::EditIndexerApiKeyInput
| ActiveLidarrBlock::EditIndexerSeedRatioInput
| ActiveLidarrBlock::EditIndexerTagsInput => {
self.app.push_navigation_stack(selected_block.into());
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveLidarrBlock::EditIndexerPriorityInput => self
.app
.push_navigation_stack(ActiveLidarrBlock::EditIndexerPriorityInput.into()),
ActiveLidarrBlock::EditIndexerToggleEnableRss => {
let indexer = self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap();
indexer.enable_rss = Some(!indexer.enable_rss.unwrap_or_default());
}
ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch => {
let indexer = self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap();
indexer.enable_automatic_search =
Some(!indexer.enable_automatic_search.unwrap_or_default());
}
ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch => {
let indexer = self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap();
indexer.enable_interactive_search =
Some(!indexer.enable_interactive_search.unwrap_or_default());
}
_ => (),
}
}
ActiveLidarrBlock::EditIndexerNameInput
| ActiveLidarrBlock::EditIndexerUrlInput
| ActiveLidarrBlock::EditIndexerApiKeyInput
| ActiveLidarrBlock::EditIndexerSeedRatioInput
| ActiveLidarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveLidarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(),
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditIndexerPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
self.app.data.lidarr_data.edit_indexer_modal = None;
}
ActiveLidarrBlock::EditIndexerNameInput
| ActiveLidarrBlock::EditIndexerUrlInput
| ActiveLidarrBlock::EditIndexerApiKeyInput
| ActiveLidarrBlock::EditIndexerSeedRatioInput
| ActiveLidarrBlock::EditIndexerPriorityInput
| ActiveLidarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => self.app.pop_navigation_stack(),
}
}
fn handle_char_key_event(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditIndexerNameInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.name
);
}
ActiveLidarrBlock::EditIndexerUrlInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.url
);
}
ActiveLidarrBlock::EditIndexerApiKeyInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.api_key
);
}
ActiveLidarrBlock::EditIndexerSeedRatioInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.seed_ratio
);
}
ActiveLidarrBlock::EditIndexerTagsInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.tags
);
}
ActiveLidarrBlock::EditIndexerPrompt => {
if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::EditIndexerConfirmPrompt
&& matches_key!(confirm, self.key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::EditIndexer(self.build_edit_indexer_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,209 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS,
};
use crate::models::servarr_models::IndexerSettings;
use crate::network::lidarr_network::LidarrEvent;
use crate::{handle_prompt_left_right_keys, matches_key};
#[cfg(test)]
#[path = "edit_indexer_settings_handler_tests.rs"]
mod edit_indexer_settings_handler_tests;
pub(super) struct IndexerSettingsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl IndexerSettingsHandler<'_, '_> {
fn build_edit_indexer_settings_params(&mut self) -> IndexerSettings {
self
.app
.data
.lidarr_data
.indexer_settings
.take()
.expect("IndexerSettings is None")
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for IndexerSettingsHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
INDEXER_SETTINGS_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>,
) -> IndexerSettingsHandler<'a, 'b> {
IndexerSettingsHandler {
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.indexer_settings.is_some()
}
fn handle_scroll_up(&mut self) {
let indexer_settings = self.app.data.lidarr_data.indexer_settings.as_mut().unwrap();
match self.active_lidarr_block {
ActiveLidarrBlock::AllIndexerSettingsPrompt => {
self.app.data.lidarr_data.selected_block.up();
}
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput => {
indexer_settings.minimum_age += 1;
}
ActiveLidarrBlock::IndexerSettingsRetentionInput => {
indexer_settings.retention += 1;
}
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput => {
indexer_settings.maximum_size += 1;
}
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => {
indexer_settings.rss_sync_interval += 1;
}
_ => (),
}
}
fn handle_scroll_down(&mut self) {
let indexer_settings = self.app.data.lidarr_data.indexer_settings.as_mut().unwrap();
match self.active_lidarr_block {
ActiveLidarrBlock::AllIndexerSettingsPrompt => {
self.app.data.lidarr_data.selected_block.down()
}
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput => {
if indexer_settings.minimum_age > 0 {
indexer_settings.minimum_age -= 1;
}
}
ActiveLidarrBlock::IndexerSettingsRetentionInput => {
if indexer_settings.retention > 0 {
indexer_settings.retention -= 1;
}
}
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput => {
if indexer_settings.maximum_size > 0 {
indexer_settings.maximum_size -= 1;
}
}
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => {
if indexer_settings.rss_sync_interval > 0 {
indexer_settings.rss_sync_interval -= 1;
}
}
_ => (),
}
}
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::AllIndexerSettingsPrompt {
handle_prompt_left_right_keys!(
self,
ActiveLidarrBlock::IndexerSettingsConfirmPrompt,
lidarr_data
);
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AllIndexerSettingsPrompt => {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::IndexerSettingsConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(
LidarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_params()),
);
self.app.should_refresh = true;
} else {
self.app.data.lidarr_data.indexer_settings = None;
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput
| ActiveLidarrBlock::IndexerSettingsRetentionInput
| ActiveLidarrBlock::IndexerSettingsMaximumSizeInput
| ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => {
self.app.push_navigation_stack(
(
self.app.data.lidarr_data.selected_block.get_active_block(),
None,
)
.into(),
)
}
_ => (),
}
}
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput
| ActiveLidarrBlock::IndexerSettingsRetentionInput
| ActiveLidarrBlock::IndexerSettingsMaximumSizeInput
| ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => self.app.pop_navigation_stack(),
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AllIndexerSettingsPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
self.app.data.lidarr_data.indexer_settings = None;
}
_ => self.app.pop_navigation_stack(),
}
}
fn handle_char_key_event(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::AllIndexerSettingsPrompt
&& self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::IndexerSettingsConfirmPrompt
&& matches_key!(confirm, self.key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::EditAllIndexerSettings(
self.build_edit_indexer_settings_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,609 @@
#[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::assert_modal_absent;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS,
};
use crate::models::servarr_models::IndexerSettings;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings;
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::INDEXER_SETTINGS_SELECTION_BLOCKS;
use crate::models::servarr_models::IndexerSettings;
use super::*;
macro_rules! test_i64_counter_scroll_value {
($block:expr, $key:expr, $data_ref:ident, $negatives:literal) => {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::new($key, &mut app, $block, None).handle();
if $key == Key::Up {
assert_eq!(
app
.data
.lidarr_data
.indexer_settings
.as_ref()
.unwrap()
.$data_ref,
1
);
} else {
if $negatives {
assert_eq!(
app
.data
.lidarr_data
.indexer_settings
.as_ref()
.unwrap()
.$data_ref,
-1
);
} else {
assert_eq!(
app
.data
.lidarr_data
.indexer_settings
.as_ref()
.unwrap()
.$data_ref,
0
);
IndexerSettingsHandler::new(Key::Up, &mut app, $block, None).handle();
assert_eq!(
app
.data
.lidarr_data
.indexer_settings
.as_ref()
.unwrap()
.$data_ref,
1
);
IndexerSettingsHandler::new($key, &mut app, $block, None).handle();
assert_eq!(
app
.data
.lidarr_data
.indexer_settings
.as_ref()
.unwrap()
.$data_ref,
0
);
}
}
};
}
#[rstest]
fn test_edit_indexer_settings_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
IndexerSettingsHandler::new(
key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
if key == Key::Up {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput
);
} else {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput
);
}
}
#[rstest]
fn test_edit_indexer_settings_prompt_scroll_no_op_when_not_ready(
#[values(Key::Up, Key::Down)] key: Key,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = true;
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
IndexerSettingsHandler::new(
key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::IndexerSettingsRetentionInput
);
}
#[rstest]
fn test_edit_indexer_settings_minimum_age_scroll(#[values(Key::Up, Key::Down)] key: Key) {
test_i64_counter_scroll_value!(
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,
key,
minimum_age,
false
);
}
#[rstest]
fn test_edit_indexer_settings_retention_scroll(#[values(Key::Up, Key::Down)] key: Key) {
test_i64_counter_scroll_value!(
ActiveLidarrBlock::IndexerSettingsRetentionInput,
key,
retention,
false
);
}
#[rstest]
fn test_edit_indexer_settings_maximum_size_scroll(#[values(Key::Up, Key::Down)] key: Key) {
test_i64_counter_scroll_value!(
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,
key,
maximum_size,
false
);
}
#[rstest]
fn test_edit_indexer_settings_rss_sync_interval_scroll(#[values(Key::Up, Key::Down)] key: Key) {
test_i64_counter_scroll_value!(
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput,
key,
rss_sync_interval,
false
);
}
}
mod test_handle_left_right_action {
use crate::models::servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
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::Indexers.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.y = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1;
IndexerSettingsHandler::new(
key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
IndexerSettingsHandler::new(
key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::{
assert_navigation_popped,
models::{
BlockSelectionState, servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
servarr_models::IndexerSettings,
},
network::lidarr_network::LidarrEvent,
};
use super::*;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_edit_indexer_settings_prompt_prompt_decline_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.should_refresh);
assert_none!(app.data.lidarr_data.indexer_settings);
}
#[test]
fn test_edit_indexer_settings_prompt_prompt_confirmation_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
app.data.lidarr_data.indexer_settings = Some(indexer_settings());
app.data.lidarr_data.prompt_confirm = true;
IndexerSettingsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::EditAllIndexerSettings(indexer_settings())
);
assert_modal_absent!(app.data.lidarr_data.indexer_settings);
assert!(app.should_refresh);
}
#[test]
fn test_edit_indexer_settings_prompt_prompt_confirmation_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.lidarr_data.prompt_confirm = true;
IndexerSettingsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(!app.should_refresh);
}
#[rstest]
#[case(ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, 0)]
#[case(ActiveLidarrBlock::IndexerSettingsRetentionInput, 1)]
#[case(ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, 2)]
#[case(ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput, 3)]
fn test_edit_indexer_settings_prompt_submit_selected_block(
#[case] selected_block: ActiveLidarrBlock,
#[case] y_index: usize,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.set_index(0, y_index);
IndexerSettingsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_navigation_pushed!(app, selected_block.into());
}
#[rstest]
fn test_edit_indexer_settings_prompt_submit_selected_block_no_op_when_not_ready(
#[values(0, 1, 2, 3, 4)] y_index: usize,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = true;
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.set_index(0, y_index);
IndexerSettingsHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::AllIndexerSettingsPrompt.into()
);
}
#[rstest]
fn test_edit_indexer_settings_selected_block_submit(
#[values(
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,
ActiveLidarrBlock::IndexerSettingsRetentionInput,
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
app.push_navigation_stack(active_lidarr_block.into());
IndexerSettingsHandler::new(SUBMIT_KEY, &mut app, active_lidarr_block, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
}
}
mod test_handle_esc {
use rstest::rstest;
use crate::models::servarr_models::IndexerSettings;
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
fn test_edit_indexer_settings_prompt_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.indexer_settings);
}
#[rstest]
fn test_edit_indexer_settings_selected_blocks_esc(
#[values(
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,
ActiveLidarrBlock::IndexerSettingsRetentionInput,
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert_some_eq_x!(
&app.data.lidarr_data.indexer_settings,
&IndexerSettings::default()
);
}
}
mod test_handle_key_char {
use crate::{
assert_navigation_popped,
models::{
BlockSelectionState, servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
},
network::lidarr_network::LidarrEvent,
};
use super::*;
#[test]
fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
app.data.lidarr_data.indexer_settings = Some(indexer_settings());
IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::EditAllIndexerSettings(indexer_settings())
);
assert_modal_absent!(app.data.lidarr_data.indexer_settings);
assert!(app.should_refresh);
}
}
#[test]
fn test_indexer_settings_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) {
assert!(IndexerSettingsHandler::accepts(active_lidarr_block));
} else {
assert!(!IndexerSettingsHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_indexer_settings_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 = IndexerSettingsHandler::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_edit_indexer_settings_params() {
let mut app = App::test_default();
app.data.lidarr_data.indexer_settings = Some(indexer_settings());
let actual_indexer_settings = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
)
.build_edit_indexer_settings_params();
assert_eq!(actual_indexer_settings, indexer_settings());
assert_modal_absent!(app.data.lidarr_data.indexer_settings);
}
#[test]
fn test_edit_indexer_settings_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = true;
let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_edit_indexer_settings_handler_not_ready_when_indexer_settings_is_none() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = false;
let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_edit_indexer_settings_handler_ready_when_not_loading_and_indexer_settings_is_some() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = false;
app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default());
let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,717 @@
#[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::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::indexers::IndexersHandler;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXERS_BLOCKS,
};
use crate::models::servarr_models::Indexer;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer;
use crate::test_handler_delegation;
mod test_handle_delete {
use pretty_assertions::assert_eq;
use super::*;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
#[test]
fn test_delete_indexer_prompt() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteIndexerPrompt.into());
}
#[test]
fn test_delete_indexer_prompt_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into());
}
}
mod test_handle_left_right_action {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
#[rstest]
fn test_indexers_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(4);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::RootFolders.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::RootFolders.into());
}
#[rstest]
fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(4);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::System.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::System.into());
}
#[rstest]
fn test_left_right_delete_indexer_prompt_toggle(
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
IndexersHandler::new(key, &mut app, ActiveLidarrBlock::DeleteIndexerPrompt, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
IndexersHandler::new(key, &mut app, ActiveLidarrBlock::DeleteIndexerPrompt, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use super::*;
use crate::assert_navigation_popped;
use crate::models::servarr_data::lidarr::lidarr_data::{
EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, LidarrData,
};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::{Indexer, IndexerField};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer;
use bimap::BiMap;
use pretty_assertions::assert_eq;
use serde_json::{Number, Value};
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[rstest]
fn test_edit_indexer_submit(#[values(true, false)] torrent_protocol: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
let protocol = if torrent_protocol {
"torrent".to_owned()
} else {
"usenet".to_owned()
};
let mut expected_edit_indexer_modal = EditIndexerModal {
name: "Test".into(),
enable_rss: Some(true),
enable_automatic_search: Some(true),
enable_interactive_search: Some(true),
url: "https://test.com".into(),
api_key: "1234".into(),
tags: "usenet, test".into(),
..EditIndexerModal::default()
};
let mut lidarr_data = LidarrData {
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
..LidarrData::default()
};
let mut fields = vec![
IndexerField {
name: Some("baseUrl".to_owned()),
value: Some(Value::String("https://test.com".to_owned())),
},
IndexerField {
name: Some("apiKey".to_owned()),
value: Some(Value::String("1234".to_owned())),
},
];
if torrent_protocol {
fields.push(IndexerField {
name: Some("seedCriteria.seedRatio".to_owned()),
value: Some(Value::from(1.2f64)),
});
expected_edit_indexer_modal.seed_ratio = "1.2".into();
}
let indexer = Indexer {
name: Some("Test".to_owned()),
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
protocol,
tags: vec![Number::from(1), Number::from(2)],
fields: Some(fields),
..Indexer::default()
};
lidarr_data.indexers.set_items(vec![indexer]);
app.data.lidarr_data = lidarr_data;
IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::EditIndexerPrompt.into());
assert_some_eq_x!(
&app.data.lidarr_data.edit_indexer_modal,
&EditIndexerModal::from(&app.data.lidarr_data)
);
assert_some_eq_x!(
&app.data.lidarr_data.edit_indexer_modal,
&expected_edit_indexer_modal
);
if torrent_protocol {
assert_eq!(
app.data.lidarr_data.selected_block.blocks,
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS
);
} else {
assert_eq!(
app.data.lidarr_data.selected_block.blocks,
EDIT_INDEXER_NZB_SELECTION_BLOCKS
);
}
}
#[test]
fn test_edit_indexer_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into());
assert_none!(app.data.lidarr_data.edit_indexer_modal);
}
#[test]
fn test_delete_indexer_prompt_confirm_submit() {
let mut app = App::test_default();
app.data.lidarr_data.indexers.set_items(vec![indexer()]);
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into());
IndexersHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteIndexerPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::DeleteIndexer(1)
);
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
}
#[test]
fn test_prompt_decline_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into());
IndexersHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteIndexerPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
fn test_delete_indexer_prompt_block_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
IndexersHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::DeleteIndexerPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[rstest]
fn test_test_indexer_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.data.lidarr_data.indexer_test_errors = Some("test result".to_owned());
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into());
IndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::TestIndexer, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert_none!(app.data.lidarr_data.indexer_test_errors);
}
#[rstest]
fn test_default_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
IndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert_is_empty!(app.error.text);
}
}
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use super::*;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer;
use crate::{
assert_navigation_popped,
models::servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
network::lidarr_network::LidarrEvent,
};
#[test]
fn test_refresh_indexers_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
IndexersHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into());
assert!(app.should_refresh);
}
#[test]
fn test_refresh_indexers_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
IndexersHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into());
assert!(!app.should_refresh);
}
#[test]
fn test_indexer_settings_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.settings.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
assert_eq!(
app.data.lidarr_data.selected_block.blocks,
INDEXER_SETTINGS_SELECTION_BLOCKS
);
}
#[test]
fn test_indexer_settings_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.settings.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into());
}
#[test]
fn test_test_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.test.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::TestIndexer.into());
}
#[test]
fn test_test_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.test.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into());
}
#[test]
fn test_test_all_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.test_all.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::TestAllIndexers.into());
}
#[test]
fn test_test_all_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.test_all.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into());
}
#[test]
fn test_delete_indexer_prompt_confirm() {
let mut app = App::test_default();
app.data.lidarr_data.indexers.set_items(vec![indexer()]);
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into());
IndexersHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::DeleteIndexerPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::DeleteIndexer(1)
);
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
}
}
#[rstest]
fn test_delegates_edit_indexer_blocks_to_edit_indexer_handler(
#[values(
ActiveLidarrBlock::EditIndexerPrompt,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
ActiveLidarrBlock::EditIndexerApiKeyInput,
ActiveLidarrBlock::EditIndexerNameInput,
ActiveLidarrBlock::EditIndexerSeedRatioInput,
ActiveLidarrBlock::EditIndexerToggleEnableRss,
ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveLidarrBlock::EditIndexerUrlInput,
ActiveLidarrBlock::EditIndexerTagsInput
)]
active_lidarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
IndexersHandler,
ActiveLidarrBlock::Indexers,
active_lidarr_block
);
}
#[rstest]
fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler(
#[values(
ActiveLidarrBlock::AllIndexerSettingsPrompt,
ActiveLidarrBlock::IndexerSettingsConfirmPrompt,
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,
ActiveLidarrBlock::IndexerSettingsRetentionInput,
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput
)]
active_lidarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
IndexersHandler,
ActiveLidarrBlock::Indexers,
active_lidarr_block
);
}
#[test]
fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() {
test_handler_delegation!(
IndexersHandler,
ActiveLidarrBlock::Indexers,
ActiveLidarrBlock::TestAllIndexers
);
}
#[test]
fn test_indexers_handler_accepts() {
let mut indexers_blocks = Vec::new();
indexers_blocks.extend(INDEXERS_BLOCKS);
indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS);
indexers_blocks.extend(EDIT_INDEXER_BLOCKS);
indexers_blocks.push(ActiveLidarrBlock::TestAllIndexers);
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if indexers_blocks.contains(&active_lidarr_block) {
assert!(IndexersHandler::accepts(active_lidarr_block));
} else {
assert!(!IndexersHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_indexers_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 = IndexersHandler::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_extract_indexer_id() {
let mut app = App::test_default();
app.data.lidarr_data.indexers.set_items(vec![indexer()]);
let indexer_id = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
)
.extract_indexer_id();
assert_eq!(indexer_id, 1);
}
#[test]
fn test_indexers_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = true;
let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_indexers_handler_not_ready_when_indexers_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = false;
let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_indexers_handler_ready_when_not_loading_and_indexers_is_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.is_loading = false;
app
.data
.lidarr_data
.indexers
.set_items(vec![Indexer::default()]);
let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Indexers,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,217 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::lidarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::lidarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
use crate::handlers::lidarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
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::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
};
use crate::models::{BlockSelectionState, Route};
use crate::network::lidarr_network::LidarrEvent;
mod edit_indexer_handler;
mod edit_indexer_settings_handler;
mod test_all_indexers_handler;
#[cfg(test)]
#[path = "indexers_handler_tests.rs"]
mod indexers_handler_tests;
pub(super) struct IndexersHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl IndexersHandler<'_, '_> {
fn extract_indexer_id(&self) -> i64 {
self.app.data.lidarr_data.indexers.current_selection().id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for IndexersHandler<'a, 'b> {
fn handle(&mut self) {
let indexers_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::Indexers.into());
if !handle_table(
self,
|app| &mut app.data.lidarr_data.indexers,
indexers_table_handling_config,
) {
match self.active_lidarr_block {
_ if EditIndexerHandler::accepts(self.active_lidarr_block) => {
EditIndexerHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle()
}
_ if IndexerSettingsHandler::accepts(self.active_lidarr_block) => {
IndexerSettingsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle()
}
_ if TestAllIndexersHandler::accepts(self.active_lidarr_block) => {
TestAllIndexersHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle()
}
_ => self.handle_key_event(),
}
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
EditIndexerHandler::accepts(active_block)
|| IndexerSettingsHandler::accepts(active_block)
|| TestAllIndexersHandler::accepts(active_block)
|| INDEXERS_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>,
) -> IndexersHandler<'a, 'b> {
IndexersHandler {
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.indexers.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::Indexers {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into());
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::Indexers => handle_change_tab_left_right_keys(self.app, self.key),
ActiveLidarrBlock::DeleteIndexerPrompt => handle_prompt_toggle(self.app, self.key),
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteIndexerPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteIndexer(self.extract_indexer_id()));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::Indexers => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into());
self.app.data.lidarr_data.edit_indexer_modal = Some((&self.app.data.lidarr_data).into());
let protocol = &self
.app
.data
.lidarr_data
.indexers
.current_selection()
.protocol;
if protocol == "torrent" {
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
} else {
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS);
}
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteIndexerPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::TestIndexer => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.indexer_test_errors = None;
}
_ => handle_clear_errors(self.app),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::Indexers => match self.key {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if matches_key!(test, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into());
}
_ if matches_key!(test_all, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into());
}
_ if matches_key!(settings, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
}
_ => (),
},
ActiveLidarrBlock::DeleteIndexerPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteIndexer(self.extract_indexer_id()));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,108 @@
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::lidarr::lidarr_data::ActiveLidarrBlock;
#[cfg(test)]
#[path = "test_all_indexers_handler_tests.rs"]
mod test_all_indexers_handler_tests;
pub(super) struct TestAllIndexersHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl TestAllIndexersHandler<'_, '_> {}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for TestAllIndexersHandler<'a, 'b> {
fn handle(&mut self) {
let indexer_test_all_results_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::TestAllIndexers.into());
if !handle_table(
self,
|app| {
app
.data
.lidarr_data
.indexer_test_all_results
.as_mut()
.unwrap()
},
indexer_test_all_results_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
active_block == ActiveLidarrBlock::TestAllIndexers
}
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>,
) -> TestAllIndexersHandler<'a, 'b> {
TestAllIndexersHandler {
key,
app,
active_lidarr_block: active_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
let table_is_ready = if let Some(table) = &self.app.data.lidarr_data.indexer_test_all_results {
!table.is_empty()
} else {
false
};
!self.app.is_loading && table_is_ready
}
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) {
if self.active_lidarr_block == ActiveLidarrBlock::TestAllIndexers {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.indexer_test_all_results = None;
}
}
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()
}
}
@@ -0,0 +1,133 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_popped;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
mod test_handle_esc {
use super::*;
const ESC_KEY: crate::event::Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
fn test_test_all_indexers_prompt_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into());
app.data.lidarr_data.indexer_test_all_results = Some(StatefulTable::default());
TestAllIndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::TestAllIndexers, None)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into());
assert_none!(app.data.lidarr_data.indexer_test_all_results);
}
}
#[test]
fn test_test_all_indexers_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if active_lidarr_block == ActiveLidarrBlock::TestAllIndexers {
assert!(TestAllIndexersHandler::accepts(active_lidarr_block));
} else {
assert!(!TestAllIndexersHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_test_all_indexers_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 = TestAllIndexersHandler::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_test_all_indexers_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into());
app.is_loading = true;
let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TestAllIndexers,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_test_all_indexers_handler_not_ready_when_results_is_none() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into());
app.is_loading = false;
let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TestAllIndexers,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_test_all_indexers_handler_not_ready_when_results_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into());
app.is_loading = false;
app.data.lidarr_data.indexer_test_all_results = Some(StatefulTable::default());
let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TestAllIndexers,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_test_all_indexers_handler_ready_when_not_loading_and_results_is_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into());
app.is_loading = false;
let mut results = StatefulTable::default();
results.set_items(vec![IndexerTestResultModalItem::default()]);
app.data.lidarr_data.indexer_test_all_results = Some(results);
let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TestAllIndexers,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,616 @@
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions, AddArtistSearchResult};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ActiveLidarrBlock,
};
use crate::models::servarr_data::lidarr::modals::AddArtistModal;
use crate::models::{BlockSelectionState, Route, Scrollable};
use crate::network::lidarr_network::LidarrEvent;
use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)]
#[path = "add_artist_handler_tests.rs"]
mod add_artist_handler_tests;
pub struct AddArtistHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl AddArtistHandler<'_, '_> {
fn build_add_artist_body(&mut self) -> AddArtistBody {
let add_artist_modal = self
.app
.data
.lidarr_data
.add_artist_modal
.take()
.expect("AddArtistModal is None");
let tags = add_artist_modal.tags.text;
let AddArtistModal {
root_folder_list,
monitor_list,
monitor_new_items_list,
quality_profile_list,
metadata_profile_list,
..
} = add_artist_modal;
let (foreign_artist_id, artist_name) = {
let AddArtistSearchResult {
foreign_artist_id,
artist_name,
..
} = self
.app
.data
.lidarr_data
.add_searched_artists
.as_ref()
.unwrap()
.current_selection();
(foreign_artist_id.clone(), artist_name.text.clone())
};
let quality_profile = quality_profile_list.current_selection();
let quality_profile_id = *self
.app
.data
.lidarr_data
.quality_profile_map
.iter()
.filter(|(_, value)| *value == quality_profile)
.map(|(key, _)| key)
.next()
.unwrap();
let metadata_profile = metadata_profile_list.current_selection();
let metadata_profile_id = *self
.app
.data
.lidarr_data
.metadata_profile_map
.iter()
.filter(|(_, value)| *value == metadata_profile)
.map(|(key, _)| key)
.next()
.unwrap();
let path = root_folder_list.current_selection().path.clone();
let monitor = *monitor_list.current_selection();
let monitor_new_items = *monitor_new_items_list.current_selection();
AddArtistBody {
foreign_artist_id,
artist_name,
monitored: true,
root_folder_path: path,
quality_profile_id,
metadata_profile_id,
tags: Vec::new(),
tag_input_string: Some(tags),
add_options: AddArtistOptions {
monitor,
monitor_new_items,
search_for_missing_albums: true,
},
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a, 'b> {
fn handle(&mut self) {
let add_artist_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::AddArtistSearchResults.into());
if !handle_table(
self,
|app| {
app
.data
.lidarr_data
.add_searched_artists
.as_mut()
.expect("add_searched_artists should be initialized")
},
add_artist_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
ADD_ARTIST_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> AddArtistHandler<'a, 'b> {
AddArtistHandler {
key,
app,
active_lidarr_block: active_block,
_context: context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading
}
fn handle_scroll_up(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_up(),
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_up(),
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_up(),
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_up(),
ActiveLidarrBlock::AddArtistSelectRootFolder => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.root_folder_list
.scroll_up(),
ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_down(),
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_down(),
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_down(),
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_down(),
ActiveLidarrBlock::AddArtistSelectRootFolder => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.root_folder_list
.scroll_down(),
ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.down(),
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSelectRootFolder => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.root_folder_list
.scroll_to_top(),
ActiveLidarrBlock::AddArtistSearchInput => self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
.scroll_home(),
ActiveLidarrBlock::AddArtistTagsInput => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.tags
.scroll_home(),
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSelectMonitor => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSelectRootFolder => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.root_folder_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddArtistSearchInput => self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
.reset_offset(),
ActiveLidarrBlock::AddArtistTagsInput => self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.tags
.reset_offset(),
_ => (),
}
}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistPrompt => handle_prompt_toggle(self.app, self.key),
ActiveLidarrBlock::AddArtistSearchInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
)
}
ActiveLidarrBlock::AddArtistTagsInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.tags
)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSearchInput
if !self
.app
.data
.lidarr_data
.add_artist_search
.as_ref()
.unwrap()
.text
.is_empty() =>
{
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveLidarrBlock::AddArtistSearchResults
if self.app.data.lidarr_data.add_searched_artists.is_some() =>
{
let foreign_artist_id = self
.app
.data
.lidarr_data
.add_searched_artists
.as_ref()
.unwrap()
.current_selection()
.foreign_artist_id
.clone();
if self
.app
.data
.lidarr_data
.artists
.items
.iter()
.any(|artist| artist.foreign_artist_id == foreign_artist_id)
{
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into());
} else {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into());
self.app.data.lidarr_data.add_artist_modal = Some((&self.app.data.lidarr_data).into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS);
}
}
ActiveLidarrBlock::AddArtistPrompt => {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::AddArtistConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::AddArtist(self.build_add_artist_body()));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.push_navigation_stack(
self
.app
.data
.lidarr_data
.selected_block
.get_active_block()
.into(),
),
ActiveLidarrBlock::AddArtistTagsInput => {
self.app.push_navigation_stack(
self
.app
.data
.lidarr_data
.selected_block
.get_active_block()
.into(),
);
self.app.ignore_special_keys_for_textbox_input = true;
}
_ => (),
}
}
ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(),
ActiveLidarrBlock::AddArtistTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSearchInput => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.add_artist_search = None;
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveLidarrBlock::AddArtistSearchResults
| ActiveLidarrBlock::AddArtistEmptySearchResults => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.add_searched_artists = None;
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveLidarrBlock::AddArtistPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.add_artist_modal = None;
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistAlreadyInLibrary
| ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(),
ActiveLidarrBlock::AddArtistTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::AddArtistSearchInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_artist_search
.as_mut()
.unwrap()
)
}
ActiveLidarrBlock::AddArtistTagsInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_artist_modal
.as_mut()
.unwrap()
.tags
)
}
ActiveLidarrBlock::AddArtistPrompt => {
if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::AddArtistConfirmPrompt
&& matches_key!(confirm, key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::AddArtist(self.build_add_artist_body()));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,468 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_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::lidarr_models::{
LidarrHistoryItem, LidarrRelease, LidarrReleaseDownloadBody, Track,
};
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
use crate::models::stateful_table::SortOption;
use crate::network::lidarr_network::LidarrEvent;
use serde_json::Number;
#[cfg(test)]
#[path = "album_details_handler_tests.rs"]
mod album_details_handler_tests;
pub(in crate::handlers::lidarr_handlers) struct AlbumDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl AlbumDetailsHandler<'_, '_> {
fn extract_track_file_id(&self) -> i64 {
self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.expect("Album details have not been loaded")
.tracks
.current_selection()
.track_file_id
}
fn extract_album_id(&self) -> i64 {
self.app.data.lidarr_data.albums.current_selection().id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AlbumDetailsHandler<'a, 'b> {
fn handle(&mut self) {
let tracks_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::AlbumDetails.into())
.searching_block(ActiveLidarrBlock::SearchTracks.into())
.search_error_block(ActiveLidarrBlock::SearchTracksError.into())
.search_field_fn(|track: &Track| &track.title);
let album_history_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::AlbumHistory.into())
.sorting_block(ActiveLidarrBlock::AlbumHistorySortPrompt.into())
.sort_options(history_sorting_options())
.searching_block(ActiveLidarrBlock::SearchAlbumHistory.into())
.search_error_block(ActiveLidarrBlock::SearchAlbumHistoryError.into())
.search_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text)
.filtering_block(ActiveLidarrBlock::FilterAlbumHistory.into())
.filter_error_block(ActiveLidarrBlock::FilterAlbumHistoryError.into())
.filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text);
let album_releases_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::ManualAlbumSearch.into())
.sorting_block(ActiveLidarrBlock::ManualAlbumSearchSortPrompt.into())
.sort_options(releases_sorting_options());
if !handle_table(
self,
|app| {
&mut app
.data
.lidarr_data
.album_details_modal
.as_mut()
.expect("Album details modal is undefined")
.tracks
},
tracks_table_handling_config,
) && !handle_table(
self,
|app| {
&mut app
.data
.lidarr_data
.album_details_modal
.as_mut()
.expect("Album details modal is undefined")
.album_history
},
album_history_table_handling_config,
) && !handle_table(
self,
|app| {
&mut app
.data
.lidarr_data
.album_details_modal
.as_mut()
.expect("Album details modal is undefined")
.album_releases
},
album_releases_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
ALBUM_DETAILS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> AlbumDetailsHandler<'a, 'b> {
AlbumDetailsHandler {
key,
app,
active_lidarr_block: active_block,
_context: context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
if self.app.is_loading {
return false;
}
let Some(album_details_modal) = &self.app.data.lidarr_data.album_details_modal else {
return false;
};
match self.active_lidarr_block {
ActiveLidarrBlock::AlbumDetails => !album_details_modal.tracks.is_empty(),
ActiveLidarrBlock::AlbumHistory => !album_details_modal.album_history.is_empty(),
ActiveLidarrBlock::ManualAlbumSearch => !album_details_modal.album_releases.is_empty(),
_ => 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) {
if self.active_lidarr_block == ActiveLidarrBlock::AlbumDetails {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteTrackFilePrompt.into());
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AlbumDetails
| ActiveLidarrBlock::AlbumHistory
| ActiveLidarrBlock::ManualAlbumSearch => match self.key {
_ if matches_key!(left, self.key) => {
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_details_tabs
.previous();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_details_tabs
.get_active_route(),
);
}
_ if matches_key!(right, self.key) => {
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_details_tabs
.next();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_details_tabs
.get_active_route(),
);
}
_ => (),
},
ActiveLidarrBlock::AutomaticallySearchAlbumPrompt
| ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt
| ActiveLidarrBlock::DeleteTrackFilePrompt => {
handle_prompt_toggle(self.app, self.key);
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AlbumDetails
if self.app.data.lidarr_data.album_details_modal.is_some()
&& !self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.tracks
.is_empty() =>
{
self
.app
.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into())
}
ActiveLidarrBlock::AlbumHistory => self
.app
.push_navigation_stack(ActiveLidarrBlock::AlbumHistoryDetails.into()),
ActiveLidarrBlock::DeleteTrackFilePrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteTrackFile(self.extract_track_file_id()));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::AutomaticallySearchAlbumPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(
LidarrEvent::TriggerAutomaticAlbumSearch(self.extract_album_id()),
);
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::ManualAlbumSearch => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt.into());
}
ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
let LidarrRelease {
guid, indexer_id, ..
} = self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_releases
.current_selection();
let params = LidarrReleaseDownloadBody {
guid: guid.clone(),
indexer_id: *indexer_id,
};
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DownloadRelease(params));
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AlbumDetails | ActiveLidarrBlock::ManualAlbumSearch => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.album_details_modal = None;
}
ActiveLidarrBlock::AlbumHistoryDetails => {
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::AlbumHistory => {
if self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_history
.filtered_items
.is_some()
{
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.album_history
.filtered_items = None;
} else {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.album_details_modal = None;
}
}
ActiveLidarrBlock::AutomaticallySearchAlbumPrompt
| ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt
| ActiveLidarrBlock::DeleteTrackFilePrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::AlbumDetails
| ActiveLidarrBlock::AlbumHistory
| ActiveLidarrBlock::ManualAlbumSearch => match self.key {
_ if matches_key!(refresh, self.key) => {
self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
_ if matches_key!(auto_search, self.key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchAlbumPrompt.into());
}
_ => (),
},
ActiveLidarrBlock::AutomaticallySearchAlbumPrompt if matches_key!(confirm, key) => {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(
LidarrEvent::TriggerAutomaticAlbumSearch(self.extract_album_id()),
);
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::DeleteTrackFilePrompt if matches_key!(confirm, key) => {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteTrackFile(self.extract_track_file_id()));
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt if matches_key!(confirm, key) => {
self.app.data.lidarr_data.prompt_confirm = true;
let LidarrRelease {
guid, indexer_id, ..
} = self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.album_releases
.current_selection();
let params = LidarrReleaseDownloadBody {
guid: guid.clone(),
indexer_id: *indexer_id,
};
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DownloadRelease(params));
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()
}
}
pub(in crate::handlers::lidarr_handlers::library) fn releases_sorting_options()
-> Vec<SortOption<LidarrRelease>> {
vec![
SortOption {
name: "Source",
cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)),
},
SortOption {
name: "Age",
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
},
SortOption {
name: "Rejected",
cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)),
},
SortOption {
name: "Title",
cmp_fn: Some(|a, b| {
a.title
.text
.to_lowercase()
.cmp(&b.title.text.to_lowercase())
}),
},
SortOption {
name: "Indexer",
cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())),
},
SortOption {
name: "Size",
cmp_fn: Some(|a, b| a.size.cmp(&b.size)),
},
SortOption {
name: "Peers",
cmp_fn: Some(|a, b| {
let default_number = Number::from(i64::MAX);
let seeder_a = a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
seeder_a.cmp(&seeder_b)
}),
},
SortOption {
name: "Quality",
cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)),
},
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,444 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_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::lidarr_models::{
Album, LidarrHistoryItem, LidarrRelease, LidarrReleaseDownloadBody,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS,
EDIT_ARTIST_SELECTION_BLOCKS,
};
use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, Route};
use crate::network::lidarr_network::LidarrEvent;
use serde_json::Number;
#[cfg(test)]
#[path = "artist_details_handler_tests.rs"]
mod artist_details_handler_tests;
pub struct ArtistDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl ArtistDetailsHandler<'_, '_> {
fn extract_artist_id(&self) -> i64 {
self.app.data.lidarr_data.artists.current_selection().id
}
fn extract_album_id(&self) -> i64 {
self.app.data.lidarr_data.albums.current_selection().id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler<'a, 'b> {
fn handle(&mut self) {
let albums_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::ArtistDetails.into())
.searching_block(ActiveLidarrBlock::SearchAlbums.into())
.search_error_block(ActiveLidarrBlock::SearchAlbumsError.into())
.search_field_fn(|album: &Album| &album.title.text);
let artist_history_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::ArtistHistory.into())
.sorting_block(ActiveLidarrBlock::ArtistHistorySortPrompt.into())
.sort_options(history_sorting_options())
.searching_block(ActiveLidarrBlock::SearchArtistHistory.into())
.search_error_block(ActiveLidarrBlock::SearchArtistHistoryError.into())
.search_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text)
.filtering_block(ActiveLidarrBlock::FilterArtistHistory.into())
.filter_error_block(ActiveLidarrBlock::FilterArtistHistoryError.into())
.filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text);
let artist_releases_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::ManualArtistSearch.into())
.sorting_block(ActiveLidarrBlock::ManualArtistSearchSortPrompt.into())
.sort_options(releases_sorting_options());
if !handle_table(
self,
|app| &mut app.data.lidarr_data.albums,
albums_table_handling_config,
) && !handle_table(
self,
|app| &mut app.data.lidarr_data.artist_history,
artist_history_table_handling_config,
) && !handle_table(
self,
|app| &mut app.data.lidarr_data.discography_releases,
artist_releases_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
ARTIST_DETAILS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
) -> ArtistDetailsHandler<'a, 'b> {
ArtistDetailsHandler {
key,
app,
active_lidarr_block: active_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
if self.app.is_loading {
return false;
}
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistHistory => !self.app.data.lidarr_data.artist_history.is_empty(),
ActiveLidarrBlock::ManualArtistSearch => {
!self.app.data.lidarr_data.discography_releases.is_empty()
}
_ => 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) {
if self.active_lidarr_block == ActiveLidarrBlock::ArtistDetails {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistDetails
| ActiveLidarrBlock::ArtistHistory
| ActiveLidarrBlock::ManualArtistSearch => match self.key {
_ if matches_key!(left, self.key) => {
self.app.data.lidarr_data.artist_info_tabs.previous();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.lidarr_data
.artist_info_tabs
.get_active_route(),
);
}
_ if matches_key!(right, self.key) => {
self.app.data.lidarr_data.artist_info_tabs.next();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.lidarr_data
.artist_info_tabs
.get_active_route(),
);
}
_ => (),
},
ActiveLidarrBlock::UpdateAndScanArtistPrompt
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt
| ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
handle_prompt_toggle(self.app, self.key);
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistDetails if !self.app.data.lidarr_data.albums.is_empty() => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
}
ActiveLidarrBlock::ArtistHistory if !self.app.data.lidarr_data.artist_history.is_empty() => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::ArtistHistoryDetails.into());
}
ActiveLidarrBlock::ManualArtistSearch => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into());
}
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
let LidarrRelease {
guid, indexer_id, ..
} = self
.app
.data
.lidarr_data
.discography_releases
.current_selection()
.clone();
let params = LidarrReleaseDownloadBody { guid, indexer_id };
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DownloadRelease(params));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(
LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()),
);
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id()));
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::UpdateAndScanArtistPrompt
| ActiveLidarrBlock::AutomaticallySearchArtistPrompt
| ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::ArtistHistoryDetails => {
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::ArtistHistory => {
if self
.app
.data
.lidarr_data
.artist_history
.filtered_items
.is_some()
{
self.app.data.lidarr_data.artist_history.reset_filter();
} else {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_artist_info_tabs();
}
}
ActiveLidarrBlock::ArtistDetails | ActiveLidarrBlock::ManualArtistSearch => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_artist_info_tabs();
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistDetails => match self.key {
_ if matches_key!(refresh, key) => self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into()),
_ if matches_key!(auto_search, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into());
}
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
}
_ if matches_key!(edit, key) => {
self.app.push_navigation_stack(
(
ActiveLidarrBlock::EditArtistPrompt,
Some(self.active_lidarr_block),
)
.into(),
);
self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
}
_ if matches_key!(toggle_monitoring, key) => {
if !self.app.data.lidarr_data.albums.is_empty() {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::ToggleAlbumMonitoring(self.extract_album_id()));
self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
}
_ => (),
},
ActiveLidarrBlock::ArtistHistory | ActiveLidarrBlock::ManualArtistSearch => match self.key {
_ if matches_key!(refresh, key) => self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into()),
_ if matches_key!(auto_search, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into());
}
_ if matches_key!(edit, key) => {
self.app.push_navigation_stack(
(
ActiveLidarrBlock::EditArtistPrompt,
Some(self.active_lidarr_block),
)
.into(),
);
self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
}
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into());
}
_ => (),
},
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(
LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()),
);
self.app.pop_navigation_stack();
}
}
ActiveLidarrBlock::UpdateAndScanArtistPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id()));
self.app.pop_navigation_stack();
}
}
ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
let LidarrRelease {
guid, indexer_id, ..
} = self
.app
.data
.lidarr_data
.discography_releases
.current_selection()
.clone();
let params = LidarrReleaseDownloadBody { guid, indexer_id };
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DownloadRelease(params));
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 releases_sorting_options() -> Vec<SortOption<LidarrRelease>> {
vec![
SortOption {
name: "Source",
cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)),
},
SortOption {
name: "Age",
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
},
SortOption {
name: "Rejected",
cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)),
},
SortOption {
name: "Title",
cmp_fn: Some(|a, b| {
a.title
.text
.to_lowercase()
.cmp(&b.title.text.to_lowercase())
}),
},
SortOption {
name: "Indexer",
cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())),
},
SortOption {
name: "Size",
cmp_fn: Some(|a, b| a.size.cmp(&b.size)),
},
SortOption {
name: "Peers",
cmp_fn: Some(|a, b| {
let default_number = Number::from(i64::MAX);
let seeder_a = a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
seeder_a.cmp(&seeder_b)
}),
},
SortOption {
name: "Quality",
cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)),
},
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
use crate::models::Route;
use crate::models::lidarr_models::DeleteParams;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_BLOCKS;
use crate::network::lidarr_network::LidarrEvent;
use crate::{
app::App,
event::Key,
handlers::{KeyEventHandler, handle_prompt_toggle},
matches_key,
models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
};
#[cfg(test)]
#[path = "delete_album_handler_tests.rs"]
mod delete_album_handler_tests;
pub(in crate::handlers::lidarr_handlers) struct DeleteAlbumHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl DeleteAlbumHandler<'_, '_> {
fn build_delete_album_params(&mut self) -> DeleteParams {
let id = self.app.data.lidarr_data.albums.current_selection().id;
let delete_files = self.app.data.lidarr_data.delete_files;
let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion;
self.app.data.lidarr_data.reset_delete_preferences();
DeleteParams {
id,
delete_files,
add_import_list_exclusion,
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteAlbumHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
DELETE_ALBUM_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
) -> Self {
DeleteAlbumHandler {
key,
app,
active_lidarr_block: active_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading
}
fn handle_scroll_up(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
self.app.data.lidarr_data.selected_block.up();
}
}
fn handle_scroll_down(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
self.app.data.lidarr_data.selected_block.down();
}
}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
handle_prompt_toggle(self.app, self.key);
}
}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::DeleteAlbumConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteAlbum(self.build_delete_album_params()));
self.app.should_refresh = true;
} else {
self.app.data.lidarr_data.reset_delete_preferences();
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile => {
self.app.data.lidarr_data.delete_files = !self.app.data.lidarr_data.delete_files;
}
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion => {
self.app.data.lidarr_data.add_import_list_exclusion =
!self.app.data.lidarr_data.add_import_list_exclusion;
}
_ => (),
}
}
}
fn handle_esc(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_delete_preferences();
self.app.data.lidarr_data.prompt_confirm = false;
}
}
fn handle_char_key_event(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt
&& self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::DeleteAlbumConfirmPrompt
&& matches_key!(confirm, self.key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteAlbum(self.build_delete_album_params()));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,404 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
use crate::models::lidarr_models::{Album, DeleteParams};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ALBUM_BLOCKS};
mod test_handle_scroll_up_and_down {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS;
use super::*;
#[rstest]
fn test_delete_album_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
let mut app = App::test_default();
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
if key == Key::Up {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile
);
} else {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteAlbumConfirmPrompt
);
}
}
#[rstest]
fn test_delete_album_prompt_scroll_no_op_when_not_ready(
#[values(Key::Up, Key::Down)] key: Key,
) {
let mut app = App::test_default();
app.is_loading = true;
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion
);
}
}
mod test_handle_left_right_action {
use rstest::rstest;
use super::*;
#[rstest]
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use pretty_assertions::assert_eq;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS;
use crate::network::lidarr_network::LidarrEvent;
use super::*;
use crate::assert_navigation_popped;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_delete_album_prompt_prompt_decline_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_album_confirm_prompt_prompt_confirmation_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
let expected_delete_album_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::DeleteAlbum(expected_delete_album_params))
);
assert!(app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_album_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::DeleteAlbumPrompt.into()
);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(app.data.lidarr_data.delete_files);
assert!(app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_album_toggle_delete_files_submit() {
let current_route = ActiveLidarrBlock::DeleteAlbumPrompt.into();
let mut app = App::test_default();
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(app.data.lidarr_data.delete_files, true);
DeleteAlbumHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(app.data.lidarr_data.delete_files, false);
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
use rstest::rstest;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
fn test_delete_album_prompt_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteAlbumHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
}
mod test_handle_key_char {
use crate::{
assert_navigation_popped,
models::{
BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS,
},
network::lidarr_network::LidarrEvent,
};
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_delete_album_confirm_prompt_prompt_confirm() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
let expected_delete_album_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1);
DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::DeleteAlbum(expected_delete_album_params))
);
assert!(app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
}
#[test]
fn test_delete_album_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) {
assert!(DeleteAlbumHandler::accepts(active_lidarr_block));
} else {
assert!(!DeleteAlbumHandler::accepts(active_lidarr_block));
}
});
}
#[rstest]
fn test_delete_album_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_delete_album_params() {
let mut app = App::test_default();
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
let expected_delete_album_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
let delete_album_params = DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.build_delete_album_params();
assert_eq!(delete_album_params, expected_delete_album_params);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_album_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.is_loading = true;
let handler = DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_delete_album_handler_ready_when_not_loading() {
let mut app = App::test_default();
app.is_loading = false;
let handler = DeleteAlbumHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,149 @@
use crate::models::Route;
use crate::models::lidarr_models::DeleteParams;
use crate::network::lidarr_network::LidarrEvent;
use crate::{
app::App,
event::Key,
handlers::{KeyEventHandler, handle_prompt_toggle},
matches_key,
models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS},
};
#[cfg(test)]
#[path = "delete_artist_handler_tests.rs"]
mod delete_artist_handler_tests;
pub(in crate::handlers::lidarr_handlers) struct DeleteArtistHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl DeleteArtistHandler<'_, '_> {
fn build_delete_artist_params(&mut self) -> DeleteParams {
let id = self.app.data.lidarr_data.artists.current_selection().id;
let delete_files = self.app.data.lidarr_data.delete_files;
let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion;
self.app.data.lidarr_data.reset_delete_preferences();
DeleteParams {
id,
delete_files,
add_import_list_exclusion,
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
DELETE_ARTIST_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
) -> Self {
DeleteArtistHandler {
key,
app,
active_lidarr_block: active_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading
}
fn handle_scroll_up(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
self.app.data.lidarr_data.selected_block.up();
}
}
fn handle_scroll_down(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
self.app.data.lidarr_data.selected_block.down();
}
}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
handle_prompt_toggle(self.app, self.key);
}
}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::DeleteArtistConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params()));
self.app.should_refresh = true;
} else {
self.app.data.lidarr_data.reset_delete_preferences();
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::DeleteArtistToggleDeleteFile => {
self.app.data.lidarr_data.delete_files = !self.app.data.lidarr_data.delete_files;
}
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion => {
self.app.data.lidarr_data.add_import_list_exclusion =
!self.app.data.lidarr_data.add_import_list_exclusion;
}
_ => (),
}
}
}
fn handle_esc(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_delete_preferences();
self.app.data.lidarr_data.prompt_confirm = false;
}
}
fn handle_char_key_event(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt
&& self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::DeleteArtistConfirmPrompt
&& matches_key!(confirm, self.key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params()));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,410 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::delete_artist_handler::DeleteArtistHandler;
use crate::models::lidarr_models::{Artist, DeleteParams};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS};
mod test_handle_scroll_up_and_down {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS;
use super::*;
#[rstest]
fn test_delete_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
let mut app = App::test_default();
app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
if key == Key::Up {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteArtistToggleDeleteFile
);
} else {
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteArtistConfirmPrompt
);
}
}
#[rstest]
fn test_delete_artist_prompt_scroll_no_op_when_not_ready(
#[values(Key::Up, Key::Down)] key: Key,
) {
let mut app = App::test_default();
app.is_loading = true;
app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
app.data.lidarr_data.selected_block.down();
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
assert_eq!(
app.data.lidarr_data.selected_block.get_active_block(),
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion
);
}
}
mod test_handle_left_right_action {
use rstest::rstest;
use super::*;
#[rstest]
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use pretty_assertions::assert_eq;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS;
use crate::network::lidarr_network::LidarrEvent;
use super::*;
use crate::assert_navigation_popped;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_delete_artist_prompt_prompt_decline_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteArtistHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_artist_confirm_prompt_prompt_confirmation_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
let expected_delete_artist_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
DeleteArtistHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::DeleteArtist(expected_delete_artist_params))
);
assert!(app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_artist_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteArtistHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::DeleteArtistPrompt.into()
);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(app.data.lidarr_data.delete_files);
assert!(app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_artist_toggle_delete_files_submit() {
let current_route = ActiveLidarrBlock::DeleteArtistPrompt.into();
let mut app = App::test_default();
app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
DeleteArtistHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(app.data.lidarr_data.delete_files, true);
DeleteArtistHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(app.data.lidarr_data.delete_files, false);
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
use rstest::rstest;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
fn test_delete_artist_prompt_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
DeleteArtistHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
}
mod test_handle_key_char {
use crate::{
assert_navigation_popped,
models::{
BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS,
},
network::lidarr_network::LidarrEvent,
};
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_delete_artist_confirm_prompt_prompt_confirm() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
let expected_delete_artist_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
app
.data
.lidarr_data
.selected_block
.set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1);
DeleteArtistHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert_eq!(
app.data.lidarr_data.prompt_confirm_action,
Some(LidarrEvent::DeleteArtist(expected_delete_artist_params))
);
assert!(app.should_refresh);
assert!(app.data.lidarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
}
#[test]
fn test_delete_artist_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) {
assert!(DeleteArtistHandler::accepts(active_lidarr_block));
} else {
assert!(!DeleteArtistHandler::accepts(active_lidarr_block));
}
});
}
#[rstest]
fn test_delete_artist_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DeleteArtistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_delete_artist_params() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.data.lidarr_data.delete_files = true;
app.data.lidarr_data.add_import_list_exclusion = true;
let expected_delete_artist_params = DeleteParams {
id: 0,
delete_files: true,
add_import_list_exclusion: true,
};
let delete_artist_params = DeleteArtistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.build_delete_artist_params();
assert_eq!(delete_artist_params, expected_delete_artist_params);
assert!(!app.data.lidarr_data.delete_files);
assert!(!app.data.lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_delete_artist_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.is_loading = true;
let handler = DeleteArtistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_delete_artist_handler_ready_when_not_loading() {
let mut app = App::test_default();
app.is_loading = false;
let handler = DeleteArtistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,455 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::lidarr_models::EditArtistParams;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS};
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::models::{Route, Scrollable};
use crate::network::lidarr_network::LidarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)]
#[path = "edit_artist_handler_tests.rs"]
mod edit_artist_handler_tests;
pub(in crate::handlers::lidarr_handlers) struct EditArtistHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl EditArtistHandler<'_, '_> {
fn build_edit_artist_params(&mut self) -> EditArtistParams {
let edit_artist_modal = self
.app
.data
.lidarr_data
.edit_artist_modal
.take()
.expect("EditArtistModal is None");
let artist_id = self.app.data.lidarr_data.artists.current_selection().id;
let tags = edit_artist_modal.tags.text;
let EditArtistModal {
monitored,
path,
monitor_list,
quality_profile_list,
metadata_profile_list,
..
} = edit_artist_modal;
let quality_profile = quality_profile_list.current_selection();
let quality_profile_id = *self
.app
.data
.lidarr_data
.quality_profile_map
.iter()
.filter(|(_, value)| *value == quality_profile)
.map(|(key, _)| key)
.next()
.unwrap();
let metadata_profile = metadata_profile_list.current_selection();
let metadata_profile_id = *self
.app
.data
.lidarr_data
.metadata_profile_map
.iter()
.filter(|(_, value)| *value == metadata_profile)
.map(|(key, _)| key)
.next()
.unwrap();
EditArtistParams {
artist_id,
monitored,
monitor_new_items: Some(*monitor_list.current_selection()),
quality_profile_id: Some(quality_profile_id),
metadata_profile_id: Some(metadata_profile_id),
root_folder_path: Some(path.text),
tag_input_string: Some(tags),
..EditArtistParams::default()
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditArtistHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
EDIT_ARTIST_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> EditArtistHandler<'a, 'b> {
EditArtistHandler {
key,
app,
active_lidarr_block: active_block,
context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading && self.app.data.lidarr_data.edit_artist_modal.is_some()
}
fn handle_scroll_up(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_up(),
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_up(),
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_up(),
ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_down(),
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_down(),
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_down(),
ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.down(),
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_top(),
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_top(),
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_top(),
ActiveLidarrBlock::EditArtistPathInput => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.path
.scroll_home(),
ActiveLidarrBlock::EditArtistTagsInput => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.tags
.scroll_home(),
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_bottom(),
ActiveLidarrBlock::EditArtistSelectQualityProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::EditArtistSelectMetadataProfile => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::EditArtistPathInput => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.path
.reset_offset(),
ActiveLidarrBlock::EditArtistTagsInput => self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.tags
.reset_offset(),
_ => (),
}
}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistPrompt => handle_prompt_toggle(self.app, self.key),
ActiveLidarrBlock::EditArtistPathInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.path
)
}
ActiveLidarrBlock::EditArtistTagsInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.tags
)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistPrompt => {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::EditArtistConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::EditArtist(self.build_edit_artist_params()));
self.app.should_refresh = true;
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
| ActiveLidarrBlock::EditArtistSelectQualityProfile
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.push_navigation_stack(
(
self.app.data.lidarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
),
ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => {
self.app.push_navigation_stack(
(
self.app.data.lidarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
);
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveLidarrBlock::EditArtistToggleMonitored => {
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitored = Some(
!self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.monitored
.unwrap_or_default(),
)
}
_ => (),
}
}
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
| ActiveLidarrBlock::EditArtistSelectQualityProfile
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(),
ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistTagsInput | ActiveLidarrBlock::EditArtistPathInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveLidarrBlock::EditArtistPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.edit_artist_modal = None;
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::EditArtistSelectMonitorNewItems
| ActiveLidarrBlock::EditArtistSelectQualityProfile
| ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(),
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::EditArtistPathInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.path
)
}
ActiveLidarrBlock::EditArtistTagsInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.edit_artist_modal
.as_mut()
.unwrap()
.tags
)
}
ActiveLidarrBlock::EditArtistPrompt => {
if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::EditArtistConfirmPrompt
&& matches_key!(confirm, key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::EditArtist(self.build_edit_artist_params()));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,772 @@
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::Number;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options};
use crate::models::lidarr_models::{Album, Artist, ArtistStatistics, ArtistStatus};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
LIBRARY_BLOCKS, TRACK_DETAILS_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_handler_delegation,
};
#[test]
fn test_library_handler_accepts() {
let mut library_handler_blocks = Vec::new();
library_handler_blocks.extend(LIBRARY_BLOCKS);
library_handler_blocks.extend(ARTIST_DETAILS_BLOCKS);
library_handler_blocks.extend(DELETE_ARTIST_BLOCKS);
library_handler_blocks.extend(DELETE_ALBUM_BLOCKS);
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
library_handler_blocks.extend(ADD_ARTIST_BLOCKS);
library_handler_blocks.extend(ALBUM_DETAILS_BLOCKS);
library_handler_blocks.extend(TRACK_DETAILS_BLOCKS);
ActiveLidarrBlock::iter().for_each(|lidarr_block| {
if library_handler_blocks.contains(&lidarr_block) {
assert!(
LibraryHandler::accepts(lidarr_block),
"{lidarr_block} is not accepted by the LibraryHandler"
);
} else {
assert!(!LibraryHandler::accepts(lidarr_block));
}
});
}
#[test]
fn test_artists_sorting_options_name() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
a.artist_name
.text
.to_lowercase()
.cmp(&b.artist_name.text.to_lowercase())
};
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[0].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Name");
}
#[test]
fn test_artists_sorting_options_type() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
a.artist_type
.as_ref()
.unwrap_or(&String::new())
.to_lowercase()
.cmp(
&b.artist_type
.as_ref()
.unwrap_or(&String::new())
.to_lowercase(),
)
};
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[1].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Type");
}
#[test]
fn test_artists_sorting_options_status() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
a.status
.to_string()
.to_lowercase()
.cmp(&b.status.to_string().to_lowercase())
};
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[2].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Status");
}
#[test]
fn test_artists_sorting_options_quality_profile() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering =
|a, b| a.quality_profile_id.cmp(&b.quality_profile_id);
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[3].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Quality Profile");
}
#[test]
fn test_artists_sorting_options_metadata_profile() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering =
|a, b| a.metadata_profile_id.cmp(&b.metadata_profile_id);
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[4].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Metadata Profile");
}
#[test]
fn test_artists_sorting_options_albums() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
a.statistics
.as_ref()
.map_or(0, |stats| stats.album_count)
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.album_count))
};
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[5].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Albums");
}
#[test]
fn test_artists_sorting_options_tracks() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
a.statistics
.as_ref()
.map_or(0, |stats| stats.track_count)
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.track_count))
};
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[6].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Tracks");
}
#[test]
fn test_artists_sorting_options_size() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
a.statistics
.as_ref()
.map_or(0, |stats| stats.size_on_disk)
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.size_on_disk))
};
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[7].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Size");
}
#[test]
fn test_artists_sorting_options_monitored() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| a.monitored.cmp(&b.monitored);
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[8].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Monitored");
}
#[test]
fn test_artists_sorting_options_tags() {
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
let a_str = a
.tags
.iter()
.map(|tag| tag.as_i64().unwrap().to_string())
.collect::<Vec<String>>()
.join(",");
let b_str = b
.tags
.iter()
.map(|tag| tag.as_i64().unwrap().to_string())
.collect::<Vec<String>>()
.join(",");
a_str.cmp(&b_str)
};
let mut expected_artists_vec = artists_vec();
expected_artists_vec.sort_by(expected_cmp_fn);
let sort_option = artists_sorting_options()[9].clone();
let mut sorted_artists_vec = artists_vec();
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_artists_vec, expected_artists_vec);
assert_str_eq!(sort_option.name, "Tags");
}
#[test]
fn test_toggle_monitoring_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.is_routing = false;
LibraryHandler::new(
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
&mut app,
ActiveLidarrBlock::Artists,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
assert!(app.data.lidarr_data.prompt_confirm);
assert!(app.is_routing);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::ToggleArtistMonitoring(0)
);
}
#[test]
fn test_toggle_monitoring_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.is_routing = false;
LibraryHandler::new(
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
&mut app,
ActiveLidarrBlock::Artists,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_modal_absent!(app.data.lidarr_data.prompt_confirm_action);
assert!(!app.is_routing);
}
#[test]
fn test_update_all_artists_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
LibraryHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Artists,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
}
#[test]
fn test_update_all_artists_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
LibraryHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Artists,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_update_all_artists_prompt_confirm_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.submit.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::UpdateAllArtists
);
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_update_all_artists_prompt_decline_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.submit.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_update_all_artists_prompt_esc() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[test]
fn test_update_all_artists_prompt_left_right() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
LibraryHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[test]
fn test_update_all_artists_prompt_confirm_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::UpdateAllArtists
);
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
}
fn artists_vec() -> Vec<Artist> {
vec![
Artist {
id: 3,
artist_name: "Test Artist 1".into(),
artist_type: Some("Group".to_owned()),
status: ArtistStatus::Ended,
quality_profile_id: 1,
metadata_profile_id: 1,
monitored: false,
tags: vec![Number::from(1), Number::from(2)],
statistics: Some(ArtistStatistics {
album_count: 5,
track_count: 50,
size_on_disk: 789,
..ArtistStatistics::default()
}),
..Artist::default()
},
Artist {
id: 2,
artist_name: "Test Artist 2".into(),
artist_type: Some("Solo".to_owned()),
status: ArtistStatus::Continuing,
quality_profile_id: 2,
metadata_profile_id: 2,
monitored: false,
tags: vec![Number::from(1), Number::from(3)],
statistics: Some(ArtistStatistics {
album_count: 10,
track_count: 100,
size_on_disk: 456,
..ArtistStatistics::default()
}),
..Artist::default()
},
Artist {
id: 1,
artist_name: "Test Artist 3".into(),
artist_type: None,
status: ArtistStatus::Deleted,
quality_profile_id: 3,
metadata_profile_id: 3,
monitored: true,
tags: vec![Number::from(2), Number::from(3)],
statistics: Some(ArtistStatistics {
album_count: 3,
track_count: 30,
size_on_disk: 123,
..ArtistStatistics::default()
}),
..Artist::default()
},
]
}
#[rstest]
fn test_delegates_add_artist_blocks_to_add_artist_handler(
#[values(
ActiveLidarrBlock::AddArtistSearchInput,
ActiveLidarrBlock::AddArtistEmptySearchResults,
ActiveLidarrBlock::AddArtistSearchResults
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(active_lidarr_block.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_delegates_delete_album_blocks_to_delete_album_handler() {
let mut app = App::test_default();
app
.data
.lidarr_data
.albums
.set_items(vec![Album::default()]);
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteAlbumPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ArtistDetails.into()
);
}
#[test]
fn test_delegates_delete_artist_blocks_to_delete_artist_handler() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteArtistPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[rstest]
fn test_delegates_edit_artist_blocks_to_edit_artist_handler(
#[values(
ActiveLidarrBlock::EditArtistPrompt,
ActiveLidarrBlock::EditArtistSelectMetadataProfile,
ActiveLidarrBlock::EditArtistSelectMonitorNewItems,
ActiveLidarrBlock::EditArtistSelectQualityProfile,
ActiveLidarrBlock::EditArtistTagsInput,
ActiveLidarrBlock::EditArtistPathInput
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(active_lidarr_block.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[rstest]
fn test_delegates_artist_details_blocks_to_artist_details_handler(
#[values(
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::SearchAlbums,
ActiveLidarrBlock::SearchAlbumsError,
ActiveLidarrBlock::UpdateAndScanArtistPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(active_lidarr_block.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[rstest]
fn test_delegates_album_details_blocks_to_album_details_handler(
#[values(
ActiveLidarrBlock::AlbumDetails,
ActiveLidarrBlock::AlbumHistory,
ActiveLidarrBlock::SearchTracks,
ActiveLidarrBlock::SearchTracksError,
ActiveLidarrBlock::AutomaticallySearchAlbumPrompt,
ActiveLidarrBlock::SearchAlbumHistory,
ActiveLidarrBlock::SearchAlbumHistoryError,
ActiveLidarrBlock::FilterAlbumHistory,
ActiveLidarrBlock::FilterAlbumHistoryError,
ActiveLidarrBlock::AlbumHistorySortPrompt,
ActiveLidarrBlock::AlbumHistoryDetails,
ActiveLidarrBlock::ManualAlbumSearch,
ActiveLidarrBlock::ManualAlbumSearchSortPrompt,
ActiveLidarrBlock::DeleteTrackFilePrompt
)]
active_sonarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LibraryHandler,
ActiveLidarrBlock::Artists,
active_sonarr_block
);
}
#[rstest]
fn test_delegates_track_details_blocks_to_track_details_handler(
#[values(
ActiveLidarrBlock::TrackDetails,
ActiveLidarrBlock::TrackHistory,
ActiveLidarrBlock::TrackHistoryDetails,
ActiveLidarrBlock::SearchTrackHistory,
ActiveLidarrBlock::SearchTrackHistoryError,
ActiveLidarrBlock::FilterTrackHistory,
ActiveLidarrBlock::FilterTrackHistoryError,
ActiveLidarrBlock::TrackHistorySortPrompt
)]
active_sonarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LibraryHandler,
ActiveLidarrBlock::AlbumDetails,
active_sonarr_block
);
}
#[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);
}
}
+353
View File
@@ -0,0 +1,353 @@
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 album_details_handler;
mod artist_details_handler;
mod delete_album_handler;
mod delete_artist_handler;
mod edit_artist_handler;
mod track_details_handler;
use crate::handlers::lidarr_handlers::library::album_details_handler::AlbumDetailsHandler;
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
use crate::handlers::lidarr_handlers::library::track_details_handler::TrackDetailsHandler;
use crate::models::Route;
pub(in crate::handlers::lidarr_handlers) use add_artist_handler::AddArtistHandler;
pub(in crate::handlers::lidarr_handlers) use artist_details_handler::ArtistDetailsHandler;
pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler;
pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler;
#[cfg(test)]
#[path = "library_handler_tests.rs"]
mod library_handler_tests;
pub(super) struct LibraryHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl LibraryHandler<'_, '_> {
fn extract_artist_id(&self) -> i64 {
self.app.data.lidarr_data.artists.current_selection().id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, 'b> {
fn handle(&mut self) {
let artists_table_handling_config = TableHandlingConfig::new(ActiveLidarrBlock::Artists.into())
.sorting_block(ActiveLidarrBlock::ArtistsSortPrompt.into())
.sort_options(artists_sorting_options())
.searching_block(ActiveLidarrBlock::SearchArtists.into())
.search_error_block(ActiveLidarrBlock::SearchArtistsError.into())
.search_field_fn(|artist| &artist.artist_name.text)
.filtering_block(ActiveLidarrBlock::FilterArtists.into())
.filter_error_block(ActiveLidarrBlock::FilterArtistsError.into())
.filter_field_fn(|artist| &artist.artist_name.text);
if !handle_table(
self,
|app| &mut app.data.lidarr_data.artists,
artists_table_handling_config,
) {
match self.active_lidarr_block {
_ if AddArtistHandler::accepts(self.active_lidarr_block) => {
AddArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if DeleteArtistHandler::accepts(self.active_lidarr_block) => {
DeleteArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if EditArtistHandler::accepts(self.active_lidarr_block) => {
EditArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if ArtistDetailsHandler::accepts(self.active_lidarr_block) => {
ArtistDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => {
DeleteAlbumHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if AlbumDetailsHandler::accepts(self.active_lidarr_block) => {
AlbumDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if TrackDetailsHandler::accepts(self.active_lidarr_block) => {
TrackDetailsHandler::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)
|| DeleteAlbumHandler::accepts(active_block)
|| EditArtistHandler::accepts(active_block)
|| ArtistDetailsHandler::accepts(active_block)
|| AlbumDetailsHandler::accepts(active_block)
|| TrackDetailsHandler::accepts(active_block)
|| LIBRARY_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> LibraryHandler<'a, 'b> {
LibraryHandler {
key,
app,
active_lidarr_block: active_block,
context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading && !self.app.data.lidarr_data.artists.is_empty()
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::Artists {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::Artists => handle_change_tab_left_right_keys(self.app, self.key),
ActiveLidarrBlock::UpdateAllArtistsPrompt => handle_prompt_toggle(self.app, self.key),
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::Artists => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
}
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
_ => {
handle_clear_errors(self.app);
}
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::Artists => match key {
_ if matches_key!(add, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into());
self.app.data.lidarr_data.add_artist_search = Some(HorizontallyScrollableText::default());
self.app.ignore_special_keys_for_textbox_input = true;
}
_ if matches_key!(toggle_monitoring, key) => {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(
LidarrEvent::ToggleArtistMonitoring(self.extract_artist_id()),
);
self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
_ if matches_key!(edit, key) => {
self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into());
self
.app
.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into());
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
}
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
}
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
fn artists_sorting_options() -> Vec<SortOption<Artist>> {
vec![
SortOption {
name: "Name",
cmp_fn: Some(|a, b| {
a.artist_name
.text
.to_lowercase()
.cmp(&b.artist_name.text.to_lowercase())
}),
},
SortOption {
name: "Type",
cmp_fn: Some(|a, b| {
a.artist_type
.as_ref()
.unwrap_or(&String::new())
.to_lowercase()
.cmp(
&b.artist_type
.as_ref()
.unwrap_or(&String::new())
.to_lowercase(),
)
}),
},
SortOption {
name: "Status",
cmp_fn: Some(|a, b| {
a.status
.to_string()
.to_lowercase()
.cmp(&b.status.to_string().to_lowercase())
}),
},
SortOption {
name: "Quality Profile",
cmp_fn: Some(|a, b| a.quality_profile_id.cmp(&b.quality_profile_id)),
},
SortOption {
name: "Metadata Profile",
cmp_fn: Some(|a, b| a.metadata_profile_id.cmp(&b.metadata_profile_id)),
},
SortOption {
name: "Albums",
cmp_fn: Some(|a, b| {
a.statistics
.as_ref()
.map_or(0, |stats| stats.album_count)
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.album_count))
}),
},
SortOption {
name: "Tracks",
cmp_fn: Some(|a, b| {
a.statistics
.as_ref()
.map_or(0, |stats| stats.track_count)
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.track_count))
}),
},
SortOption {
name: "Size",
cmp_fn: Some(|a, b| {
a.statistics
.as_ref()
.map_or(0, |stats| stats.size_on_disk)
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.size_on_disk))
}),
},
SortOption {
name: "Monitored",
cmp_fn: Some(|a, b| a.monitored.cmp(&b.monitored)),
},
SortOption {
name: "Tags",
cmp_fn: Some(|a, b| {
let a_str = a
.tags
.iter()
.map(|tag| tag.as_i64().unwrap().to_string())
.collect::<Vec<String>>()
.join(",");
let b_str = b
.tags
.iter()
.map(|tag| tag.as_i64().unwrap().to_string())
.collect::<Vec<String>>()
.join(",");
a_str.cmp(&b_str)
}),
},
]
}
@@ -0,0 +1,225 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::history::history_sorting_options;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::matches_key;
use crate::models::Route;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
#[cfg(test)]
#[path = "track_details_handler_tests.rs"]
mod track_details_handler_tests;
pub(super) struct TrackDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for TrackDetailsHandler<'a, 'b> {
fn handle(&mut self) {
let track_history_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::TrackHistory.into())
.sorting_block(ActiveLidarrBlock::TrackHistorySortPrompt.into())
.sort_options(history_sorting_options())
.searching_block(ActiveLidarrBlock::SearchTrackHistory.into())
.search_error_block(ActiveLidarrBlock::SearchTrackHistoryError.into())
.search_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text)
.filtering_block(ActiveLidarrBlock::FilterTrackHistory.into())
.filter_error_block(ActiveLidarrBlock::FilterTrackHistoryError.into())
.filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text);
if !handle_table(
self,
|app| {
&mut app
.data
.lidarr_data
.album_details_modal
.as_mut()
.expect("Album details modal is undefined")
.track_details_modal
.as_mut()
.expect("Track details modal is undefined")
.track_history
},
track_history_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
TRACK_DETAILS_BLOCKS.contains(&active_block)
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
) -> Self {
Self {
key,
app,
active_lidarr_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
if self.app.is_loading {
return false;
}
let Some(album_details_modal) = self.app.data.lidarr_data.album_details_modal.as_ref() else {
return false;
};
let Some(track_details_modal) = &album_details_modal.track_details_modal else {
return false;
};
match self.active_lidarr_block {
ActiveLidarrBlock::TrackDetails => !track_details_modal.track_details.is_empty(),
ActiveLidarrBlock::TrackHistory => !track_details_modal.track_history.is_empty(),
_ => 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) {
match self.active_lidarr_block {
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => match self.key {
_ if matches_key!(left, self.key) => {
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details_tabs
.previous();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
);
}
_ if matches_key!(right, self.key) => {
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details_tabs
.next();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route(),
);
}
_ => (),
},
_ => (),
}
}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::TrackHistory {
self
.app
.push_navigation_stack(ActiveLidarrBlock::TrackHistoryDetails.into());
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => {
self.app.pop_navigation_stack();
self
.app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal = None;
}
ActiveLidarrBlock::TrackHistoryDetails => {
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => match self.key {
_ if matches_key!(refresh, self.key) => {
self
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
_ => (),
},
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,407 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::track_details_handler::TrackDetailsHandler;
use crate::models::ScrollableText;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
mod test_handle_left_right_actions {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
#[case(ActiveLidarrBlock::TrackHistory, ActiveLidarrBlock::TrackDetails)]
fn test_track_details_tabs_left_right_action(
#[case] left_block: ActiveLidarrBlock,
#[case] right_block: ActiveLidarrBlock,
#[values(true, false)] is_ready: bool,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
app.is_loading = is_ready;
app.push_navigation_stack(right_block.into());
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details_tabs
.index = app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.tabs
.iter()
.position(|tab_route| tab_route.route == right_block.into())
.unwrap_or_default();
TrackDetailsHandler::new(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle();
assert_eq!(
app.get_current_route(),
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route()
);
assert_navigation_pushed!(app, left_block.into());
TrackDetailsHandler::new(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle();
assert_eq!(
app.get_current_route(),
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
.as_ref()
.unwrap()
.track_details_tabs
.get_active_route()
);
assert_navigation_pushed!(app, right_block.into());
}
}
mod test_handle_submit {
use super::*;
use crate::event::Key;
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_eq;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_track_history_submit() {
let mut app = App::test_default_fully_populated();
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::TrackHistoryDetails.into());
}
#[test]
fn test_track_history_submit_no_op_when_track_history_is_empty() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_history = StatefulTable::default();
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::TrackHistory.into()
);
}
#[test]
fn test_track_history_submit_no_op_when_not_ready() {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::TrackHistory.into()
);
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
use crate::event::Key;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_track_history_details_block_esc() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
app.push_navigation_stack(ActiveLidarrBlock::TrackHistoryDetails.into());
TrackDetailsHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::TrackHistoryDetails,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::TrackHistory.into());
}
#[rstest]
fn test_track_details_tabs_esc(
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
app.push_navigation_stack(active_lidarr_block.into());
TrackDetailsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::AlbumDetails.into());
assert_none!(
app
.data
.lidarr_data
.album_details_modal
.as_ref()
.unwrap()
.track_details_modal
);
}
}
mod test_handle_key_char {
use super::*;
use pretty_assertions::assert_eq;
#[rstest]
fn test_refresh_key(
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.is_routing = false;
TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_navigation_pushed!(app, active_lidarr_block.into());
assert!(app.is_routing);
}
#[rstest]
fn test_refresh_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(active_lidarr_block.into());
app.is_routing = false;
TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_lidarr_block.into());
assert!(!app.is_routing);
}
}
#[test]
fn test_track_details_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) {
assert!(TrackDetailsHandler::accepts(active_lidarr_block));
} else {
assert!(!TrackDetailsHandler::accepts(active_lidarr_block));
}
});
}
#[rstest]
fn test_track_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = TrackDetailsHandler::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_track_details_handler_is_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
app.is_loading = true;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_track_details_handler_is_not_ready_when_album_details_modal_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_track_details_handler_is_not_ready_when_track_details_modal_is_empty() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal = None;
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_track_details_handler_is_not_ready_when_track_details_is_empty() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_details = ScrollableText::default();
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_track_details_handler_is_not_ready_when_track_history_table_is_empty() {
let mut app = App::test_default_fully_populated();
app
.data
.lidarr_data
.album_details_modal
.as_mut()
.unwrap()
.track_details_modal
.as_mut()
.unwrap()
.track_history = StatefulTable::default();
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::TrackHistory,
None,
);
assert!(!handler.is_ready());
}
#[rstest]
fn test_track_details_handler_is_ready(
#[values(
ActiveLidarrBlock::TrackDetails,
ActiveLidarrBlock::TrackHistory,
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.is_loading = false;
let handler = TrackDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,275 @@
#[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, handle_change_tab_left_right_keys};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::{assert_navigation_pushed, test_handler_delegation};
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]
#[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)]
#[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)]
#[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)]
#[case(5, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)]
fn test_lidarr_handler_change_tab_left_right_keys(
#[case] index: usize,
#[case] left_block: ActiveLidarrBlock,
#[case] right_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.data.lidarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.key);
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
left_block.into()
);
assert_navigation_pushed!(app, left_block.into());
app.data.lidarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.key);
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
right_block.into()
);
assert_navigation_pushed!(app, right_block.into());
}
#[rstest]
#[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)]
#[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)]
#[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)]
#[case(5, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)]
fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation(
#[case] index: usize,
#[case] left_block: ActiveLidarrBlock,
#[case] right_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.data.lidarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap());
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
left_block.into()
);
assert_navigation_pushed!(app, left_block.into());
app.data.lidarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.alt.unwrap());
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
right_block.into()
);
assert_navigation_pushed!(app, right_block.into());
}
#[rstest]
#[case(0, ActiveLidarrBlock::Artists)]
#[case(1, ActiveLidarrBlock::Downloads)]
#[case(2, ActiveLidarrBlock::History)]
#[case(3, ActiveLidarrBlock::RootFolders)]
#[case(4, ActiveLidarrBlock::Indexers)]
#[case(5, ActiveLidarrBlock::System)]
fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key(
#[case] index: usize,
#[case] block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(block.into());
app.ignore_special_keys_for_textbox_input = true;
app.data.lidarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap());
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
block.into()
);
assert_eq!(app.get_current_route(), block.into());
app.data.lidarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.alt.unwrap());
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
block.into()
);
assert_eq!(app.get_current_route(), block.into());
}
#[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,
) {
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::Artists,
active_lidarr_block
);
}
#[rstest]
fn test_delegates_downloads_blocks_to_downloads_handler(
#[values(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::Downloads,
active_lidarr_block
);
}
#[rstest]
fn test_delegates_history_blocks_to_history_handler(
#[values(
ActiveLidarrBlock::History,
ActiveLidarrBlock::HistoryItemDetails,
ActiveLidarrBlock::HistorySortPrompt,
ActiveLidarrBlock::FilterHistory,
ActiveLidarrBlock::FilterHistoryError,
ActiveLidarrBlock::SearchHistory,
ActiveLidarrBlock::SearchHistoryError
)]
active_lidarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::History,
active_lidarr_block
);
}
#[rstest]
fn test_delegates_root_folders_blocks_to_root_folders_handler(
#[values(
ActiveLidarrBlock::RootFolders,
ActiveLidarrBlock::AddRootFolderPrompt,
ActiveLidarrBlock::DeleteRootFolderPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::RootFolders,
active_lidarr_block
);
}
#[rstest]
fn test_delegates_indexers_blocks_to_indexers_handler(
#[values(
ActiveLidarrBlock::DeleteIndexerPrompt,
ActiveLidarrBlock::Indexers,
ActiveLidarrBlock::AllIndexerSettingsPrompt,
ActiveLidarrBlock::IndexerSettingsConfirmPrompt,
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,
ActiveLidarrBlock::IndexerSettingsRetentionInput,
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput
)]
active_sonarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::Indexers,
active_sonarr_block
);
}
#[rstest]
fn test_delegates_system_blocks_to_system_handler(
#[values(
ActiveLidarrBlock::System,
ActiveLidarrBlock::SystemLogs,
ActiveLidarrBlock::SystemQueuedEvents,
ActiveLidarrBlock::SystemTasks,
ActiveLidarrBlock::SystemTaskStartConfirmPrompt,
ActiveLidarrBlock::SystemUpdates
)]
active_sonarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::System,
active_sonarr_block
);
}
}
+128
View File
@@ -0,0 +1,128 @@
use history::HistoryHandler;
use indexers::IndexersHandler;
use library::LibraryHandler;
use super::KeyEventHandler;
use crate::handlers::lidarr_handlers::downloads::DownloadsHandler;
use crate::handlers::lidarr_handlers::root_folders::RootFoldersHandler;
use crate::handlers::lidarr_handlers::system::SystemHandler;
use crate::models::Route;
use crate::{
app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
};
mod downloads;
mod history;
mod indexers;
mod library;
mod root_folders;
mod system;
#[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();
}
_ if DownloadsHandler::accepts(self.active_lidarr_block) => {
DownloadsHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ if HistoryHandler::accepts(self.active_lidarr_block) => {
HistoryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ if RootFoldersHandler::accepts(self.active_lidarr_block) => {
RootFoldersHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
_ if IndexersHandler::accepts(self.active_lidarr_block) => {
IndexersHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ if SystemHandler::accepts(self.active_lidarr_block) => {
SystemHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ => self.handle_key_event(),
}
}
fn accepts(_active_block: ActiveLidarrBlock) -> bool {
true
}
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 ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
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());
}
_ => (),
}
}
@@ -0,0 +1,533 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::lidarr_models::AddLidarrRootFolderBody;
use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock};
use crate::models::servarr_data::lidarr::modals::AddRootFolderModal;
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 = "add_root_folder_handler_tests.rs"]
mod add_root_folder_handler_tests;
pub(super) struct AddRootFolderHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl AddRootFolderHandler<'_, '_> {
fn build_add_root_folder_body(&mut self) -> AddLidarrRootFolderBody {
let add_root_folder_modal = self
.app
.data
.lidarr_data
.add_root_folder_modal
.take()
.expect("AddRootFolderModal is None");
let tags = add_root_folder_modal.tags.text.clone();
let AddRootFolderModal {
name,
path,
monitor_list,
monitor_new_items_list,
quality_profile_list,
metadata_profile_list,
..
} = add_root_folder_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();
AddLidarrRootFolderBody {
name: name.text,
path: path.text,
default_quality_profile_id: quality_profile_id,
default_metadata_profile_id: metadata_profile_id,
default_monitor_option: *monitor_list.current_selection(),
default_new_item_monitor_option: *monitor_new_items_list.current_selection(),
default_tags: Vec::new(),
tag_input_string: Some(tags),
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddRootFolderHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
ADD_ROOT_FOLDER_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>,
) -> AddRootFolderHandler<'a, 'b> {
AddRootFolderHandler {
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.add_root_folder_modal.is_some()
}
fn handle_scroll_up(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_up(),
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_up(),
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_up(),
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_up(),
ActiveLidarrBlock::AddRootFolderPrompt => self.app.data.lidarr_data.selected_block.up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_down(),
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_down(),
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_down(),
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_down(),
ActiveLidarrBlock::AddRootFolderPrompt => self.app.data.lidarr_data.selected_block.down(),
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_top(),
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_to_top(),
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_top(),
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_top(),
ActiveLidarrBlock::AddRootFolderNameInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.name
.scroll_home(),
ActiveLidarrBlock::AddRootFolderPathInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.path
.scroll_home(),
ActiveLidarrBlock::AddRootFolderTagsInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.tags
.scroll_home(),
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddRootFolderNameInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.name
.reset_offset(),
ActiveLidarrBlock::AddRootFolderPathInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.path
.reset_offset(),
ActiveLidarrBlock::AddRootFolderTagsInput => self
.app
.data
.lidarr_data
.add_root_folder_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::AddRootFolderPrompt => handle_prompt_toggle(self.app, self.key),
ActiveLidarrBlock::AddRootFolderNameInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.name
)
}
ActiveLidarrBlock::AddRootFolderPathInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.path
)
}
ActiveLidarrBlock::AddRootFolderTagsInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.tags
)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderPrompt => {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::AddRootFolderConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder(
self.build_add_root_folder_body(),
));
self.app.should_refresh = true;
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::AddRootFolderSelectMonitor
| ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems
| ActiveLidarrBlock::AddRootFolderSelectQualityProfile
| ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => {
self.app.push_navigation_stack(
(
self.app.data.lidarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
)
}
ActiveLidarrBlock::AddRootFolderNameInput
| ActiveLidarrBlock::AddRootFolderPathInput
| ActiveLidarrBlock::AddRootFolderTagsInput => {
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::AddRootFolderSelectMonitor
| ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems
| ActiveLidarrBlock::AddRootFolderSelectQualityProfile
| ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self.app.pop_navigation_stack(),
ActiveLidarrBlock::AddRootFolderNameInput
| ActiveLidarrBlock::AddRootFolderPathInput
| ActiveLidarrBlock::AddRootFolderTagsInput => {
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::AddRootFolderNameInput
| ActiveLidarrBlock::AddRootFolderPathInput
| ActiveLidarrBlock::AddRootFolderTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveLidarrBlock::AddRootFolderPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.add_root_folder_modal = None;
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::AddRootFolderSelectMonitor
| ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems
| ActiveLidarrBlock::AddRootFolderSelectQualityProfile
| ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self.app.pop_navigation_stack(),
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderNameInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.name
)
}
ActiveLidarrBlock::AddRootFolderPathInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.path
)
}
ActiveLidarrBlock::AddRootFolderTagsInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.tags
)
}
ActiveLidarrBlock::AddRootFolderPrompt => {
if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::AddRootFolderConfirmPrompt
&& matches_key!(confirm, key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder(
self.build_add_root_folder_body(),
));
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,173 @@
use add_root_folder_handler::AddRootFolderHandler;
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_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::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ADD_ROOT_FOLDER_SELECTION_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS,
};
use crate::models::servarr_data::lidarr::modals::AddRootFolderModal;
use crate::models::{BlockSelectionState, Route};
use crate::network::lidarr_network::LidarrEvent;
mod add_root_folder_handler;
#[cfg(test)]
#[path = "root_folders_handler_tests.rs"]
mod root_folders_handler_tests;
pub(super) struct RootFoldersHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl RootFoldersHandler<'_, '_> {
fn extract_root_folder_id(&self) -> i64 {
self
.app
.data
.lidarr_data
.root_folders
.current_selection()
.id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for RootFoldersHandler<'a, 'b> {
fn handle(&mut self) {
let root_folders_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::RootFolders.into());
if AddRootFolderHandler::accepts(self.active_lidarr_block) {
return AddRootFolderHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
if !handle_table(
self,
|app| &mut app.data.lidarr_data.root_folders,
root_folders_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
ROOT_FOLDERS_BLOCKS.contains(&active_block) || ADD_ROOT_FOLDER_BLOCKS.contains(&active_block)
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> RootFoldersHandler<'a, 'b> {
RootFoldersHandler {
key,
app,
active_lidarr_block: active_block,
context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
!self.app.is_loading && !self.app.data.lidarr_data.root_folders.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::RootFolders {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into())
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::RootFolders => handle_change_tab_left_right_keys(self.app, self.key),
ActiveLidarrBlock::DeleteRootFolderPrompt => handle_prompt_toggle(self.app, self.key),
_ => (),
}
}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::DeleteRootFolderPrompt {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteRootFolder(self.extract_root_folder_id()));
}
self.app.pop_navigation_stack();
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteRootFolderPrompt => {
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::RootFolders => match self.key {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if matches_key!(add, key) => {
self.app.data.lidarr_data.add_root_folder_modal =
Some(AddRootFolderModal::from(&self.app.data.lidarr_data));
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS);
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into());
}
_ => (),
},
ActiveLidarrBlock::DeleteRootFolderPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteRootFolder(self.extract_root_folder_id()));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,480 @@
#[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::assert_modal_present;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::root_folders::RootFoldersHandler;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS,
};
use crate::models::servarr_models::RootFolder;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder;
mod test_handle_delete {
use pretty_assertions::assert_eq;
use super::*;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
#[test]
fn test_delete_root_folder_prompt() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app
.data
.lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
RootFoldersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteRootFolderPrompt.into());
}
#[test]
fn test_delete_root_folder_prompt_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app
.data
.lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
RootFoldersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::RootFolders.into()
);
}
}
mod test_handle_left_right_action {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_root_folders_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(3);
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::History.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::History.into());
}
#[rstest]
fn test_root_folders_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(3);
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Indexers.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into());
}
#[rstest]
fn test_left_right_delete_root_folder_prompt_toggle(
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
RootFoldersHandler::new(
key,
&mut app,
ActiveLidarrBlock::DeleteRootFolderPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
RootFoldersHandler::new(
key,
&mut app,
ActiveLidarrBlock::DeleteRootFolderPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use crate::network::lidarr_network::LidarrEvent;
use super::*;
use crate::assert_navigation_popped;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_delete_root_folder_prompt_confirm_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.root_folders
.set_items(vec![root_folder()]);
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into());
RootFoldersHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteRootFolderPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::DeleteRootFolder(1)
);
assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into());
}
#[test]
fn test_delete_root_folder_prompt_decline_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into());
RootFoldersHandler::new(
SUBMIT_KEY,
&mut app,
ActiveLidarrBlock::DeleteRootFolderPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into());
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
use rstest::rstest;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_delete_root_folder_prompt_block_esc() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
RootFoldersHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::DeleteRootFolderPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into());
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[rstest]
fn test_default_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
RootFoldersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into());
assert_is_empty!(app.error.text);
}
}
mod test_handle_key_char {
use super::*;
use crate::assert_navigation_popped;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder;
use pretty_assertions::assert_eq;
#[test]
fn test_root_folder_add() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app
.data
.lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.add.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::AddRootFolderPrompt.into());
assert_modal_present!(app.data.lidarr_data.add_root_folder_modal);
}
#[test]
fn test_root_folder_add_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app
.data
.lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.add.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::RootFolders.into()
);
assert_none!(app.data.lidarr_data.add_root_folder_modal);
}
#[test]
fn test_refresh_root_folders_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::RootFolders.into());
assert!(app.should_refresh);
}
#[test]
fn test_refresh_root_folders_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app
.data
.lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::RootFolders.into()
);
assert!(!app.should_refresh);
}
#[test]
fn test_delete_root_folder_prompt_confirm() {
let mut app = App::test_default();
app
.data
.lidarr_data
.root_folders
.set_items(vec![root_folder()]);
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into());
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::DeleteRootFolderPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::DeleteRootFolder(1)
);
assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into());
}
}
#[test]
fn test_root_folders_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if ROOT_FOLDERS_BLOCKS.contains(&active_lidarr_block)
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block)
{
assert!(RootFoldersHandler::accepts(active_lidarr_block));
} else {
assert!(!RootFoldersHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_root_folders_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 = RootFoldersHandler::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_extract_root_folder_id() {
let mut app = App::test_default();
app
.data
.lidarr_data
.root_folders
.set_items(vec![root_folder()]);
let root_folder_id = RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::DeleteRootFolderPrompt,
None,
)
.extract_root_folder_id();
assert_eq!(root_folder_id, 1);
}
#[test]
fn test_root_folders_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.is_loading = true;
let handler = RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_root_folders_handler_not_ready_when_root_folders_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.is_loading = false;
let handler = RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_root_folders_handler_ready_when_not_loading_and_root_folders_is_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
app.is_loading = false;
app
.data
.lidarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
let handler = RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::RootFolders,
None,
);
assert!(handler.is_ready());
}
}
+135
View File
@@ -0,0 +1,135 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::lidarr_handlers::system::system_details_handler::SystemDetailsHandler;
use crate::handlers::{KeyEventHandler, handle_clear_errors};
use crate::matches_key;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::{Route, Scrollable};
mod system_details_handler;
#[cfg(test)]
#[path = "system_handler_tests.rs"]
mod system_handler_tests;
pub(super) struct SystemHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for SystemHandler<'a, 'b> {
fn handle(&mut self) {
match self.active_lidarr_block {
_ if SystemDetailsHandler::accepts(self.active_lidarr_block) => {
SystemDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle()
}
_ => self.handle_key_event(),
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
SystemDetailsHandler::accepts(active_block) || active_block == ActiveLidarrBlock::System
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> SystemHandler<'a, 'b> {
SystemHandler {
key,
app,
active_lidarr_block: active_block,
context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
!self.app.is_loading
&& !self.app.data.lidarr_data.logs.is_empty()
&& !self.app.data.lidarr_data.queued_events.is_empty()
&& !self.app.data.lidarr_data.tasks.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) {}
fn handle_left_right_action(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::System {
handle_change_tab_left_right_keys(self.app, self.key);
}
}
fn handle_submit(&mut self) {}
fn handle_esc(&mut self) {
handle_clear_errors(self.app)
}
fn handle_char_key_event(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::System {
let key = self.key;
match self.key {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if matches_key!(events, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::SystemQueuedEvents.into());
}
_ if matches_key!(logs, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::SystemLogs.into());
self
.app
.data
.lidarr_data
.log_details
.set_items(self.app.data.lidarr_data.logs.items.to_vec());
self.app.data.lidarr_data.log_details.scroll_to_bottom();
}
_ if matches_key!(tasks, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into());
}
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::SystemUpdates.into());
}
_ => (),
}
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -0,0 +1,207 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
use crate::models::lidarr_models::LidarrTaskName;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::models::stateful_list::StatefulList;
use crate::models::{Route, Scrollable};
use crate::network::lidarr_network::LidarrEvent;
#[cfg(test)]
#[path = "system_details_handler_tests.rs"]
mod system_details_handler_tests;
pub(super) struct SystemDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl SystemDetailsHandler<'_, '_> {
fn extract_task_name(&self) -> LidarrTaskName {
self
.app
.data
.lidarr_data
.tasks
.current_selection()
.task_name
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for SystemDetailsHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
SYSTEM_DETAILS_BLOCKS.contains(&active_block)
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> SystemDetailsHandler<'a, 'b> {
SystemDetailsHandler {
key,
app,
active_lidarr_block: active_block,
_context: context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
!self.app.is_loading
&& (!self.app.data.lidarr_data.log_details.is_empty()
|| !self.app.data.lidarr_data.tasks.is_empty()
|| !self.app.data.lidarr_data.updates.is_empty())
}
fn handle_scroll_up(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_up(),
ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_up(),
ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_up(),
ActiveLidarrBlock::SystemQueuedEvents => self.app.data.lidarr_data.queued_events.scroll_up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_down(),
ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_down(),
ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_down(),
ActiveLidarrBlock::SystemQueuedEvents => {
self.app.data.lidarr_data.queued_events.scroll_down()
}
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_to_top(),
ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_to_top(),
ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_to_top(),
ActiveLidarrBlock::SystemQueuedEvents => {
self.app.data.lidarr_data.queued_events.scroll_to_top()
}
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_to_bottom(),
ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_to_bottom(),
ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_to_bottom(),
ActiveLidarrBlock::SystemQueuedEvents => {
self.app.data.lidarr_data.queued_events.scroll_to_bottom()
}
_ => (),
}
}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::SystemLogs => match self.key {
_ if matches_key!(left, key) => {
self
.app
.data
.lidarr_data
.log_details
.items
.iter()
.for_each(|log| log.scroll_right());
}
_ if matches_key!(right, key) => {
self
.app
.data
.lidarr_data
.log_details
.items
.iter()
.for_each(|log| log.scroll_left());
}
_ => (),
},
ActiveLidarrBlock::SystemTaskStartConfirmPrompt => handle_prompt_toggle(self.app, self.key),
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::SystemTasks => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into());
}
ActiveLidarrBlock::SystemTaskStartConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::StartTask(self.extract_task_name()));
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::SystemLogs => {
self.app.data.lidarr_data.log_details = StatefulList::default();
self.app.pop_navigation_stack()
}
ActiveLidarrBlock::SystemQueuedEvents
| ActiveLidarrBlock::SystemTasks
| ActiveLidarrBlock::SystemUpdates => self.app.pop_navigation_stack(),
ActiveLidarrBlock::SystemTaskStartConfirmPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
if SYSTEM_DETAILS_BLOCKS.contains(&self.active_lidarr_block) && matches_key!(refresh, self.key)
{
self.app.should_refresh = true;
}
if self.active_lidarr_block == ActiveLidarrBlock::SystemTaskStartConfirmPrompt
&& matches_key!(confirm, self.key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::StartTask(self.extract_task_name()));
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,580 @@
#[cfg(test)]
mod tests {
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::system::SystemHandler;
use crate::models::lidarr_models::LidarrTask;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS,
};
use crate::models::servarr_models::QueueEvent;
use crate::test_handler_delegation;
mod test_handle_left_right_action {
use pretty_assertions::assert_eq;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_system_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(5);
SystemHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Indexers.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into());
}
#[rstest]
fn test_system_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(5);
SystemHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Artists.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into());
}
}
mod test_handle_esc {
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
fn test_default_esc(#[values(true, false)] is_loading: bool) {
let mut app = App::test_default();
app.is_loading = is_loading;
app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.push_navigation_stack(ActiveLidarrBlock::System.into());
SystemHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::System, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::System.into());
assert_is_empty!(app.error.text);
}
}
mod test_handle_key_char {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::models::HorizontallyScrollableText;
use super::*;
#[test]
fn test_update_system_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
SystemHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::SystemUpdates.into());
}
#[test]
fn test_update_system_key_no_op_if_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
SystemHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into());
}
#[test]
fn test_queued_events_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
SystemHandler::new(
DEFAULT_KEYBINDINGS.events.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::SystemQueuedEvents.into());
}
#[test]
fn test_queued_events_key_no_op_if_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
SystemHandler::new(
DEFAULT_KEYBINDINGS.events.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into());
}
#[test]
fn test_refresh_system_key() {
let mut app = App::test_default();
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
app.push_navigation_stack(ActiveLidarrBlock::System.into());
SystemHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::System.into());
assert!(app.should_refresh);
}
#[test]
fn test_refresh_system_key_no_op_if_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
app.push_navigation_stack(ActiveLidarrBlock::System.into());
SystemHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into());
assert!(!app.should_refresh);
}
#[test]
fn test_logs_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
SystemHandler::new(
DEFAULT_KEYBINDINGS.logs.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::SystemLogs.into());
assert_eq!(
app.data.lidarr_data.log_details.items,
app.data.lidarr_data.logs.items
);
assert_str_eq!(
app.data.lidarr_data.log_details.current_selection().text,
"test 2"
);
}
#[test]
fn test_logs_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
SystemHandler::new(
DEFAULT_KEYBINDINGS.logs.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into());
assert_is_empty!(app.data.lidarr_data.log_details);
}
#[test]
fn test_tasks_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
SystemHandler::new(
DEFAULT_KEYBINDINGS.tasks.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::SystemTasks.into());
}
#[test]
fn test_tasks_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.data.lidarr_data.logs.set_items(vec![
HorizontallyScrollableText::from("test 1"),
HorizontallyScrollableText::from("test 2"),
]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
SystemHandler::new(
DEFAULT_KEYBINDINGS.tasks.key,
&mut app,
ActiveLidarrBlock::System,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into());
}
}
#[rstest]
fn test_delegates_system_details_blocks_to_system_details_handler(
#[values(
ActiveLidarrBlock::SystemLogs,
ActiveLidarrBlock::SystemQueuedEvents,
ActiveLidarrBlock::SystemTasks,
ActiveLidarrBlock::SystemTaskStartConfirmPrompt,
ActiveLidarrBlock::SystemUpdates
)]
active_lidarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
SystemHandler,
ActiveLidarrBlock::System,
active_lidarr_block
);
}
#[test]
fn test_system_handler_accepts() {
let mut system_blocks = vec![ActiveLidarrBlock::System];
system_blocks.extend(SYSTEM_DETAILS_BLOCKS);
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if system_blocks.contains(&active_lidarr_block) {
assert!(SystemHandler::accepts(active_lidarr_block));
} else {
assert!(!SystemHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_system_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 = SystemHandler::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_system_handler_is_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.is_loading = true;
let system_handler = SystemHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::System,
None,
);
assert!(!system_handler.is_ready());
}
#[test]
fn test_system_handler_is_not_ready_when_logs_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.is_loading = false;
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
let system_handler = SystemHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::System,
None,
);
assert!(!system_handler.is_ready());
}
#[test]
fn test_system_handler_is_not_ready_when_tasks_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.is_loading = false;
app.data.lidarr_data.logs.set_items(vec!["test".into()]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
let system_handler = SystemHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::System,
None,
);
assert!(!system_handler.is_ready());
}
#[test]
fn test_system_handler_is_not_ready_when_queued_events_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.is_loading = false;
app.data.lidarr_data.logs.set_items(vec!["test".into()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
let system_handler = SystemHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::System,
None,
);
assert!(!system_handler.is_ready());
}
#[test]
fn test_system_handler_is_ready_when_all_required_tables_are_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::System.into());
app.is_loading = false;
app.data.lidarr_data.logs.set_items(vec!["test".into()]);
app
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask::default()]);
app
.data
.lidarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
let system_handler = SystemHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::System,
None,
);
assert!(system_handler.is_ready());
}
}
+8
View File
@@ -1,3 +1,4 @@
use lidarr_handlers::LidarrHandler;
use radarr_handlers::RadarrHandler; use radarr_handlers::RadarrHandler;
use sonarr_handlers::SonarrHandler; use sonarr_handlers::SonarrHandler;
@@ -15,6 +16,7 @@ use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route}; use crate::models::{HorizontallyScrollableText, Route};
mod keybinding_handler; mod keybinding_handler;
mod lidarr_handlers;
mod radarr_handlers; mod radarr_handlers;
mod sonarr_handlers; mod sonarr_handlers;
@@ -125,6 +127,9 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
Route::Sonarr(active_sonarr_block, context) => { Route::Sonarr(active_sonarr_block, context) => {
SonarrHandler::new(key, app, active_sonarr_block, context).handle() SonarrHandler::new(key, app, active_sonarr_block, context).handle()
} }
Route::Lidarr(active_lidarr_block, context) => {
LidarrHandler::new(key, app, active_lidarr_block, context).handle()
}
_ => (), _ => (),
} }
} }
@@ -187,6 +192,9 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
Route::Sonarr(_, _) => { Route::Sonarr(_, _) => {
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
} }
Route::Lidarr(_, _) => {
app.data.lidarr_data.prompt_confirm = !app.data.lidarr_data.prompt_confirm
}
_ => (), _ => (),
}, },
_ => (), _ => (),
@@ -91,9 +91,9 @@ mod tests {
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::RootFolders.into() ActiveRadarrBlock::History.into()
); );
assert_navigation_pushed!(app, ActiveRadarrBlock::RootFolders.into()); assert_navigation_pushed!(app, ActiveRadarrBlock::History.into());
} }
#[rstest] #[rstest]
@@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key; use crate::matches_key;
use crate::models::Route;
use crate::models::radarr_models::BlocklistItem; use crate::models::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
@@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
self.app self.app
} }
fn current_route(&self) -> crate::models::Route { fn current_route(&self) -> Route {
self.app.get_current_route() self.app.get_current_route()
} }
} }
@@ -3,12 +3,12 @@ use crate::event::Key;
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::matches_key; use crate::matches_key;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
}; };
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use crate::models::{BlockSelectionState, Route};
#[cfg(test)] #[cfg(test)]
#[path = "collection_details_handler_tests.rs"] #[path = "collection_details_handler_tests.rs"]
@@ -148,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
self.app self.app
} }
fn current_route(&self) -> crate::models::Route { fn current_route(&self) -> Route {
self.app.get_current_route() self.app.get_current_route()
} }
} }
@@ -1,10 +1,10 @@
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::Scrollable;
use crate::models::radarr_models::EditCollectionParams; use crate::models::radarr_models::EditCollectionParams;
use crate::models::servarr_data::radarr::modals::EditCollectionModal; use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
use crate::models::{Route, Scrollable};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
@@ -376,7 +376,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
self.app self.app
} }
fn current_route(&self) -> crate::models::Route { fn current_route(&self) -> Route {
self.app.get_current_route() self.app.get_current_route()
} }
} }
@@ -6,12 +6,12 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key; use crate::matches_key;
use crate::models::BlockSelectionState;
use crate::models::radarr_models::Collection; use crate::models::radarr_models::Collection;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
}; };
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, Route};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
mod collection_details_handler; mod collection_details_handler;
@@ -179,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
self.app self.app
} }
fn current_route(&self) -> crate::models::Route { fn current_route(&self) -> Route {
self.app.get_current_route() self.app.get_current_route()
} }
} }
@@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key; use crate::matches_key;
use crate::models::Route;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
@@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
self.app self.app
} }
fn current_route(&self) -> crate::models::Route { fn current_route(&self) -> Route {
self.app.get_current_route() self.app.get_current_route()
} }
} }

Some files were not shown because too many files have changed in this diff Show More