Compare commits

..

3 Commits

182 changed files with 16128 additions and 476 deletions
+5
View File
@@ -124,3 +124,8 @@ pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
+18 -9
View File
@@ -4,7 +4,7 @@ mod test {
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_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::models::servarr_data::ActiveKeybindingBlock;
@@ -268,6 +268,21 @@ mod test {
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]
fn test_servarr_context_clue_provider_delegates_to_radarr_provider() {
let mut app = App::test_default();
@@ -275,10 +290,7 @@ mod test {
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(
context_clues,
&crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
}
#[test]
@@ -288,10 +300,7 @@ mod test {
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(
context_clues,
&crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
}
#[test]
+28 -4
View File
@@ -1,12 +1,13 @@
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, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
EDIT_ARTIST_BLOCKS,
EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS,
};
#[cfg(test)]
@@ -57,6 +58,24 @@ pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
(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.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.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub(in crate::app) struct LidarrContextClueProvider;
impl ContextClueProvider for LidarrContextClueProvider {
@@ -71,10 +90,14 @@ impl ContextClueProvider for LidarrContextClueProvider {
.lidarr_data
.artist_info_tabs
.get_active_route_contextual_help(),
ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults => {
Some(&BARE_POPUP_CONTEXT_CLUES)
}
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)
@@ -90,6 +113,7 @@ impl ContextClueProvider for LidarrContextClueProvider {
_ 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
+68 -4
View File
@@ -3,14 +3,16 @@ 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, ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
LidarrContextClueProvider,
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES,
ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, LidarrData,
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS,
INDEXER_SETTINGS_BLOCKS, LidarrData,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use rstest::rstest;
@@ -143,6 +145,55 @@ mod tests {
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.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.update, DEFAULT_KEYBINDINGS.update.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());
}
#[rstest]
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_artist_info_tabs(
@@ -204,7 +255,8 @@ mod tests {
fn test_lidarr_context_clue_provider_bare_popup_context_clues(
#[values(
ActiveLidarrBlock::AddArtistSearchInput,
ActiveLidarrBlock::AddArtistEmptySearchResults
ActiveLidarrBlock::AddArtistEmptySearchResults,
ActiveLidarrBlock::TestAllIndexers
)]
active_lidarr_block: ActiveLidarrBlock,
) {
@@ -220,6 +272,8 @@ mod tests {
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();
@@ -262,4 +316,14 @@ mod tests {
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[test]
fn test_sonarr_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);
}
}
+360 -2
View File
@@ -1,7 +1,9 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::models::lidarr_models::Artist;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
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::artist;
@@ -30,7 +32,7 @@ mod tests {
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into());
assert!(!app.data.sonarr_data.prompt_confirm);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
@@ -48,7 +50,31 @@ mod tests {
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into());
assert!(!app.data.sonarr_data.prompt_confirm);
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);
}
@@ -130,6 +156,319 @@ mod tests {
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_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();
@@ -139,6 +478,14 @@ mod tests {
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();
@@ -146,4 +493,15 @@ mod tests {
assert_eq!(app.extract_artist_id().await, 1);
}
#[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);
}
}
+53 -1
View File
@@ -38,6 +38,13 @@ impl App<'_> {
.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::AddArtistSearchResults => {
self
.dispatch_network_event(
@@ -55,6 +62,47 @@ impl App<'_> {
.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;
}
_ => (),
}
@@ -68,7 +116,7 @@ impl App<'_> {
.lidarr_data
.add_artist_search
.as_ref()
.expect("add_artist_search should be set")
.expect("Add artist search is empty")
.text
.clone()
}
@@ -77,6 +125,10 @@ impl App<'_> {
self.data.lidarr_data.artists.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;
+1 -5
View File
@@ -1,6 +1,7 @@
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;
@@ -82,11 +83,6 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(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] = [
(DEFAULT_KEYBINDINGS.submit, "show overview/add movie"),
(DEFAULT_KEYBINDINGS.edit, "edit collection"),
+2 -1
View File
@@ -5,12 +5,13 @@ mod tests {
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_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::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTION_DETAILS_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::sonarr::sonarr_data::ActiveSonarrBlock;
+1 -5
View File
@@ -1,5 +1,6 @@
use crate::app::context_clues::{
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::models::Route;
@@ -163,11 +164,6 @@ pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [
(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;
impl ContextClueProvider for SonarrContextClueProvider {
+2 -1
View File
@@ -4,6 +4,7 @@ mod tests {
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::sonarr::sonarr_context_clues::{
SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, SonarrContextClueProvider,
@@ -15,7 +16,7 @@ mod tests {
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES,
SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES,
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES,
},
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
+12
View File
@@ -42,6 +42,11 @@ pub enum LidarrDeleteCommand {
#[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)]
@@ -120,6 +125,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm
.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
@@ -179,6 +179,39 @@ mod tests {
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 =
@@ -354,6 +387,32 @@ mod tests {
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;
+199 -2
View File
@@ -4,6 +4,10 @@ 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},
@@ -11,14 +15,46 @@ use crate::{
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "edit_command_handler_tests.rs"]
mod edit_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrEditCommand {
#[command(
about = "Edit 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(
@@ -80,6 +116,97 @@ pub enum LidarrEditCommand {
#[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 {
@@ -109,6 +236,34 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandH
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,
@@ -139,6 +294,48 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandH
.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)
@@ -32,6 +32,96 @@ mod tests {
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"]);
@@ -249,6 +339,253 @@ mod tests {
};
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 {
@@ -258,6 +595,7 @@ mod tests {
use serde_json::json;
use tokio::sync::Mutex;
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
use crate::{
app::App,
cli::{
@@ -271,6 +609,63 @@ mod tests {
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 {
@@ -405,5 +800,59 @@ mod tests {
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);
}
}
}
+9
View File
@@ -27,6 +27,8 @@ pub enum LidarrGetCommand {
)]
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(
@@ -78,6 +80,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHan
.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
@@ -49,6 +49,14 @@ mod tests {
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 =
@@ -143,6 +151,34 @@ mod tests {
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;
+221 -14
View File
@@ -21,6 +21,16 @@ mod tests {
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() {
@@ -124,6 +134,44 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "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 =
@@ -148,6 +196,30 @@ mod tests {
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 {
@@ -158,9 +230,12 @@ mod tests {
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::refresh_command_handler::LidarrRefreshCommand;
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
use crate::models::lidarr_models::LidarrTaskName;
use crate::models::servarr_models::IndexerSettings;
use crate::{
app::App,
cli::{
@@ -259,6 +334,64 @@ mod tests {
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();
@@ -303,6 +436,38 @@ mod tests {
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_toggle_artist_monitoring_command() {
let mut mock_network = MockNetworkTrait::new();
@@ -359,13 +524,13 @@ mod tests {
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{
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::TriggerAutomaticArtistSearch(1).into(),
LidarrEvent::StartTask(expected_task_name).into(),
))
.times(1)
.returning(|_| {
@@ -374,18 +539,60 @@ mod tests {
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist {
artist_id: 1,
});
let start_task_command = LidarrCommand::StartTask {
task_name: LidarrTaskName::ApplicationUpdateCheck,
};
let result = LidarrCliHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
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);
}
+81 -2
View File
@@ -27,6 +27,15 @@ pub enum LidarrListCommand {
)]
artist_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")]
@@ -39,14 +48,32 @@ pub enum LidarrListCommand {
#[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 = "List all Lidarr updates")]
Updates,
}
impl From<LidarrListCommand> for Command {
@@ -56,7 +83,7 @@ impl From<LidarrListCommand> for Command {
}
pub(super) struct LidarrListCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrListCommand,
network: &'a mut dyn NetworkTrait,
}
@@ -68,7 +95,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrListCommandHandler {
_app: app,
app,
command,
network,
}
@@ -83,6 +110,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.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
@@ -104,6 +138,30 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.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
@@ -118,6 +176,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.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
@@ -132,6 +197,20 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.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::Updates => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetUpdates.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
@@ -27,9 +27,13 @@ mod tests {
fn test_list_commands_have_no_arg_requirements(
#[values(
"artists",
"indexers",
"metadata-profiles",
"quality-profiles",
"queued-events",
"tags",
"tasks",
"updates",
"root-folders"
)]
subcommand: &str,
@@ -65,6 +69,39 @@ mod tests {
assert_eq!(album_command, expected_args);
}
#[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 =
@@ -110,6 +147,31 @@ mod tests {
};
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);
}
}
mod handler {
@@ -132,10 +194,14 @@ mod tests {
#[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,
@@ -182,6 +248,32 @@ mod tests {
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;
@@ -233,5 +325,33 @@ mod tests {
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);
}
}
}
+43 -2
View File
@@ -14,11 +14,11 @@ use trigger_automatic_search_command_handler::{
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
};
use super::{CliCommandHandler, Command};
use crate::models::lidarr_models::LidarrTaskName;
use crate::network::lidarr_network::LidarrEvent;
use crate::{app::App, network::NetworkTrait};
use super::{CliCommandHandler, Command};
mod add_command_handler;
mod delete_command_handler;
mod edit_command_handler;
@@ -86,6 +86,25 @@ pub enum LidarrCommand {
)]
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"
)]
@@ -190,6 +209,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
.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
+2 -2
View File
@@ -9,8 +9,8 @@ use crate::{
cli::{CliCommandHandler, Command, mutex_flags_or_option},
models::{
Serdeable,
servarr_models::EditIndexerParams,
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
servarr_models::{EditIndexerParams, IndexerSettings},
sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable},
},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
+2 -2
View File
@@ -622,8 +622,8 @@ mod tests {
},
models::{
Serdeable,
servarr_models::EditIndexerParams,
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
servarr_models::{EditIndexerParams, IndexerSettings},
sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable},
},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
+3 -2
View File
@@ -266,9 +266,10 @@ mod tests {
},
models::{
Serdeable,
servarr_models::IndexerSettings,
sonarr_models::{
BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody,
SonarrSerdeable, SonarrTaskName,
BlocklistItem, BlocklistResponse, Series, SonarrReleaseDownloadBody, SonarrSerdeable,
SonarrTaskName,
},
},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
@@ -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());
}
}
@@ -1,10 +1,11 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_handlers::history::history_sorting_options;
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key;
use crate::models::lidarr_models::Album;
use crate::models::lidarr_models::{Album, LidarrHistoryItem};
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS,
EDIT_ARTIST_SELECTION_BLOCKS,
@@ -41,10 +42,32 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
.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);
if !handle_table(
self,
|app| &mut app.data.lidarr_data.albums,
albums_table_handling_config,
) && !handle_table(
self,
|app| {
app
.data
.lidarr_data
.artist_history
.as_mut()
.expect("Artist history is undefined")
},
artist_history_table_handling_config,
) {
match self.active_lidarr_block {
_ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => {
@@ -83,7 +106,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
}
fn is_ready(&self) -> bool {
!self.app.is_loading
if self.active_lidarr_block == ActiveLidarrBlock::ArtistHistory {
!self.app.is_loading && self.app.data.lidarr_data.artist_history.is_some()
} else {
!self.app.is_loading
}
}
fn handle_scroll_up(&mut self) {}
@@ -106,6 +133,31 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistDetails | ActiveLidarrBlock::ArtistHistory => 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 => {
handle_prompt_toggle(self.app, self.key);
@@ -116,6 +168,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::ArtistHistory
if !self
.app
.data
.lidarr_data
.artist_history
.as_ref()
.expect("Artist history should be Some")
.is_empty() =>
{
self
.app
.push_navigation_stack(ActiveLidarrBlock::ArtistHistoryDetails.into());
}
ActiveLidarrBlock::AutomaticallySearchArtistPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(
@@ -144,6 +210,33 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
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
.as_ref()
.expect("Artist history is not populated")
.filtered_items
.is_some()
{
self
.app
.data
.lidarr_data
.artist_history
.as_mut()
.expect("Artist history is not populated")
.reset_filter();
} else {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_artist_info_tabs();
}
}
ActiveLidarrBlock::ArtistDetails => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.reset_artist_info_tabs();
@@ -194,6 +287,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
}
_ => (),
},
ActiveLidarrBlock::ArtistHistory => 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;
@@ -11,6 +11,7 @@ mod tests {
use crate::models::servarr_data::lidarr::lidarr_data::{
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
mod test_handle_delete {
use super::*;
@@ -80,13 +81,15 @@ mod tests {
mod test_handle_submit {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_popped;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::StatefulTable;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
use crate::{assert_navigation_popped, assert_navigation_pushed};
use rstest::rstest;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
@@ -138,6 +141,46 @@ mod tests {
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
assert_none!(app.data.lidarr_data.prompt_confirm_action);
}
#[test]
fn test_artist_history_submit() {
let mut app = App::test_default();
let mut artist_history = StatefulTable::default();
artist_history.set_items(vec![LidarrHistoryItem::default()]);
app.data.lidarr_data.artist_history = Some(artist_history);
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::ArtistHistoryDetails.into());
}
#[test]
fn test_artist_history_submit_no_op_when_artist_history_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into());
app.data.lidarr_data.artist_history = Some(StatefulTable::default());
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ArtistHistory.into()
);
}
#[test]
fn test_artist_history_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
}
mod test_handle_esc {
@@ -147,11 +190,71 @@ mod tests {
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::StatefulTable;
use ratatui::widgets::TableState;
use rstest::rstest;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_artist_history_details_block_esc() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into());
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistoryDetails.into());
ArtistDetailsHandler::new(
ESC_KEY,
&mut app,
ActiveLidarrBlock::ArtistHistoryDetails,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistHistory.into());
}
#[test]
fn test_artist_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() {
let mut app = App::test_default();
let artist_history = StatefulTable {
filter: Some("Test".into()),
filtered_items: Some(vec![LidarrHistoryItem::default()]),
filtered_state: Some(TableState::default()),
..StatefulTable::default()
};
app.data.lidarr_data.artist_history = Some(artist_history);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into());
ArtistDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None).handle();
assert_eq!(
app.get_current_route(),
ActiveLidarrBlock::ArtistHistory.into()
);
assert_none!(app.data.lidarr_data.artist_history.as_ref().unwrap().filter);
assert_none!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.filtered_items
);
assert_none!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.filtered_state
);
}
#[rstest]
fn test_artist_details_esc(
#[values(
@@ -191,7 +294,8 @@ mod tests {
#[rstest]
fn test_artist_details_edit_key(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
@@ -209,12 +313,12 @@ mod tests {
app,
(
ActiveLidarrBlock::EditArtistPrompt,
Some(ActiveLidarrBlock::ArtistDetails)
Some(active_lidarr_block)
)
.into()
);
assert_modal_present!(app.data.lidarr_data.edit_artist_modal);
assert!(app.data.lidarr_data.edit_artist_modal.is_some());
assert_some!(app.data.lidarr_data.edit_artist_modal);
assert_eq!(
app.data.lidarr_data.selected_block.blocks,
EDIT_ARTIST_SELECTION_BLOCKS
@@ -223,7 +327,8 @@ mod tests {
#[rstest]
fn test_artist_details_edit_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.is_loading = true;
@@ -315,7 +420,8 @@ mod tests {
#[rstest]
fn test_artist_details_auto_search_key(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
@@ -336,7 +442,8 @@ mod tests {
#[rstest]
fn test_artist_details_auto_search_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
@@ -355,7 +462,8 @@ mod tests {
#[rstest]
fn test_artist_details_update_key(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
@@ -373,7 +481,8 @@ mod tests {
#[rstest]
fn test_artist_details_update_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
@@ -392,7 +501,8 @@ mod tests {
#[rstest]
fn test_artist_details_refresh_key(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.is_routing = false;
@@ -413,7 +523,8 @@ mod tests {
#[rstest]
fn test_artist_details_refresh_key_no_op_when_not_ready(
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.is_loading = true;
@@ -444,7 +555,8 @@ mod tests {
fn test_artist_details_prompt_confirm_key(
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
#[values(ActiveLidarrBlock::ArtistDetails)] active_lidarr_block: ActiveLidarrBlock,
#[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
@@ -562,4 +674,35 @@ mod tests {
assert!(handler.is_ready());
}
#[test]
fn test_artist_details_handler_is_not_ready_when_not_loading_and_artist_history_is_none() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
let handler = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ArtistHistory,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_artist_details_handler_ready_when_not_loading_and_artist_history_is_some() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.data.lidarr_data.artist_history = Some(StatefulTable::default());
let handler = ArtistDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::ArtistHistory,
None,
);
assert!(handler.is_ready());
}
}
@@ -52,10 +52,12 @@ mod tests {
}
#[rstest]
#[case(0, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Downloads)]
#[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)]
#[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Artists)]
#[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,
@@ -84,10 +86,12 @@ mod tests {
}
#[rstest]
#[case(0, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Downloads)]
#[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)]
#[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Artists)]
#[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,
@@ -120,6 +124,8 @@ mod tests {
#[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,
@@ -226,4 +232,44 @@ mod tests {
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
);
}
}
+18 -8
View File
@@ -1,22 +1,26 @@
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 history;
mod library;
mod downloads;
mod history;
mod indexers;
mod library;
mod root_folders;
mod system;
#[cfg(test)]
#[path = "lidarr_handler_tests.rs"]
mod lidarr_handler_tests;
mod root_folders;
pub(super) struct LidarrHandler<'a, 'b> {
key: Key,
@@ -41,6 +45,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b
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(),
}
}
@@ -49,10 +59,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b
true
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -71,6 +77,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
true
}
@@ -105,9 +105,9 @@ mod tests {
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Artists.into()
ActiveLidarrBlock::Indexers.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into());
assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into());
}
#[rstest]
+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());
}
}
@@ -125,7 +125,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
.edit_indexer_modal
.as_mut()
.unwrap();
if edit_indexer_modal.priority > 0 {
if edit_indexer_modal.priority > 1 {
edit_indexer_modal.priority -= 1;
}
}
@@ -50,7 +50,7 @@ mod tests {
.as_ref()
.unwrap()
.priority,
1
2
);
} else {
assert_eq!(
@@ -61,7 +61,7 @@ mod tests {
.as_ref()
.unwrap()
.priority,
0
1
);
EditIndexerHandler::new(
@@ -80,7 +80,7 @@ mod tests {
.as_ref()
.unwrap()
.priority,
1
2
);
EditIndexerHandler::new(
@@ -98,7 +98,7 @@ mod tests {
.as_ref()
.unwrap()
.priority,
0
1
);
}
}
@@ -124,7 +124,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
.edit_indexer_modal
.as_mut()
.unwrap();
if edit_indexer_modal.priority > 0 {
if edit_indexer_modal.priority > 1 {
edit_indexer_modal.priority -= 1;
}
}
@@ -50,7 +50,7 @@ mod tests {
.as_ref()
.unwrap()
.priority,
1
2
);
} else {
assert_eq!(
@@ -61,7 +61,7 @@ mod tests {
.as_ref()
.unwrap()
.priority,
0
1
);
EditIndexerHandler::new(
@@ -80,7 +80,7 @@ mod tests {
.as_ref()
.unwrap()
.priority,
1
2
);
EditIndexerHandler::new(
@@ -98,7 +98,7 @@ mod tests {
.as_ref()
.unwrap()
.priority,
0
1
);
}
}
@@ -5,7 +5,7 @@ use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS,
};
use crate::models::sonarr_models::IndexerSettings;
use crate::models::servarr_models::IndexerSettings;
use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_prompt_left_right_keys, matches_key};
@@ -15,7 +15,7 @@ mod tests {
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS,
};
use crate::models::sonarr_models::IndexerSettings;
use crate::models::servarr_models::IndexerSettings;
mod test_handle_scroll_up_and_down {
use pretty_assertions::assert_eq;
@@ -23,7 +23,7 @@ mod tests {
use crate::models::BlockSelectionState;
use crate::models::servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS;
use crate::models::sonarr_models::IndexerSettings;
use crate::models::servarr_models::IndexerSettings;
use super::*;
@@ -242,7 +242,7 @@ mod tests {
assert_navigation_popped,
models::{
BlockSelectionState, servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
sonarr_models::IndexerSettings,
servarr_models::IndexerSettings,
},
network::sonarr_network::SonarrEvent,
};
@@ -415,7 +415,7 @@ mod tests {
mod test_handle_esc {
use rstest::rstest;
use crate::models::sonarr_models::IndexerSettings;
use crate::models::servarr_models::IndexerSettings;
use super::*;
use crate::assert_navigation_popped;
@@ -2,13 +2,14 @@
#[macro_use]
pub(in crate::handlers::sonarr_handlers) mod utils {
use crate::models::HorizontallyScrollableText;
use crate::models::servarr_models::IndexerSettings;
use crate::models::servarr_models::{
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
};
use crate::models::sonarr_models::{
AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadRecord, DownloadStatus,
Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, Season, SeasonStatistics, Series,
SeriesStatistics, SeriesStatus, SeriesType,
Episode, EpisodeFile, MediaInfo, Rating, Season, SeasonStatistics, Series, SeriesStatistics,
SeriesStatus, SeriesType,
};
use chrono::DateTime;
use serde_json::{Number, json};
+56 -9
View File
@@ -1,18 +1,21 @@
use super::{
HorizontallyScrollableText, Serdeable,
servarr_models::{
DiskSpace, HostConfig, Indexer, IndexerTestResult, QualityProfile, QualityWrapper, RootFolder,
SecurityConfig, Tag,
},
};
use crate::models::servarr_models::{IndexerSettings, LogResponse, QueueEvent, Update};
use crate::serde_enum_from;
use chrono::{DateTime, Utc};
use clap::ValueEnum;
use derivative::Derivative;
use enum_display_style_derive::EnumDisplayStyle;
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use std::fmt::{Display, Formatter};
use strum::{Display, EnumIter};
use super::{
HorizontallyScrollableText, Serdeable,
servarr_models::{
DiskSpace, HostConfig, QualityProfile, QualityWrapper, RootFolder, SecurityConfig, Tag,
},
};
use crate::serde_enum_from;
#[cfg(test)]
#[path = "lidarr_models_tests.rs"]
mod lidarr_models_tests;
@@ -425,6 +428,42 @@ pub struct LidarrHistoryItem {
pub data: LidarrHistoryData,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LidarrTask {
pub name: String,
pub task_name: LidarrTaskName,
#[serde(deserialize_with = "super::from_i64")]
pub interval: i64,
pub last_execution: DateTime<Utc>,
pub next_execution: DateTime<Utc>,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)]
#[serde(rename_all = "PascalCase")]
pub enum LidarrTaskName {
#[default]
ApplicationUpdateCheck,
Backup,
CheckHealth,
Housekeeping,
ImportListSync,
MessagingCleanup,
RefreshArtist,
RefreshMonitoredDownloads,
RescanFolders,
RssSync,
}
impl Display for LidarrTaskName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let task_name = serde_json::to_string(&self)
.expect("Unable to serialize task name")
.replace('"', "");
write!(f, "{task_name}")
}
}
impl From<LidarrSerdeable> for Serdeable {
fn from(value: LidarrSerdeable) -> Serdeable {
Serdeable::Lidarr(value)
@@ -440,15 +479,23 @@ serde_enum_from!(
Artists(Vec<Artist>),
DiskSpaces(Vec<DiskSpace>),
DownloadsResponse(DownloadsResponse),
HistoryWrapper(LidarrHistoryWrapper),
LidarrHistoryWrapper(LidarrHistoryWrapper),
LidarrHistoryItems(Vec<LidarrHistoryItem>),
HostConfig(HostConfig),
IndexerSettings(IndexerSettings),
Indexers(Vec<Indexer>),
IndexerTestResults(Vec<IndexerTestResult>),
LogResponse(LogResponse),
MetadataProfiles(Vec<MetadataProfile>),
QualityProfiles(Vec<QualityProfile>),
QueueEvents(Vec<QueueEvent>),
RootFolders(Vec<RootFolder>),
SecurityConfig(SecurityConfig),
SystemStatus(SystemStatus),
Tag(Tag),
Tags(Vec<Tag>),
Tasks(Vec<LidarrTask>),
Updates(Vec<Update>),
Value(Value),
}
);
+112 -5
View File
@@ -6,11 +6,12 @@ mod tests {
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, DownloadRecord, DownloadStatus, DownloadsResponse,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, Member, MetadataProfile,
MonitorType, NewItemMonitorType, SystemStatus,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, Member,
MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus,
};
use crate::models::servarr_models::{
DiskSpace, HostConfig, QualityProfile, RootFolder, SecurityConfig, Tag,
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
};
use crate::models::{
Serdeable,
@@ -304,7 +305,7 @@ mod tests {
}
#[test]
fn test_lidarr_serdeable_from_history_wrapper() {
fn test_lidarr_serdeable_from_lidarr_history_wrapper() {
let history_wrapper = LidarrHistoryWrapper {
records: vec![LidarrHistoryItem {
id: 1,
@@ -316,10 +317,80 @@ mod tests {
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::HistoryWrapper(history_wrapper)
LidarrSerdeable::LidarrHistoryWrapper(history_wrapper)
);
}
#[test]
fn test_lidarr_serdeable_from_lidarr_history_items() {
let history_items = vec![LidarrHistoryItem {
id: 1,
..LidarrHistoryItem::default()
}];
let lidarr_serdeable: LidarrSerdeable = history_items.clone().into();
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::LidarrHistoryItems(history_items)
);
}
#[test]
fn test_lidarr_serdeable_from_indexers() {
let indexers = vec![Indexer {
id: 1,
..Indexer::default()
}];
let lidarr_serdeable: LidarrSerdeable = indexers.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Indexers(indexers));
}
#[test]
fn test_lidarr_serdeable_from_indexer_settings() {
let indexer_settings = IndexerSettings {
id: 1,
..IndexerSettings::default()
};
let lidarr_serdeable: LidarrSerdeable = indexer_settings.clone().into();
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::IndexerSettings(indexer_settings)
);
}
#[test]
fn test_lidarr_serdeable_from_indexer_test_results() {
let indexer_test_results = vec![IndexerTestResult {
id: 1,
..IndexerTestResult::default()
}];
let lidarr_serdeable: LidarrSerdeable = indexer_test_results.clone().into();
assert_eq!(
lidarr_serdeable,
LidarrSerdeable::IndexerTestResults(indexer_test_results)
);
}
#[test]
fn test_lidarr_serdeable_from_log_response() {
let log_response = LogResponse {
records: vec![Log {
level: "info".to_owned(),
..Log::default()
}],
};
let lidarr_serdeable: LidarrSerdeable = log_response.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::LogResponse(log_response));
}
#[test]
fn test_lidarr_serdeable_from_metadata_profiles() {
let metadata_profiles = vec![MetadataProfile {
@@ -362,6 +433,18 @@ mod tests {
);
}
#[test]
fn test_lidarr_serdeable_from_queue_events() {
let queue_events = vec![QueueEvent {
trigger: "test".to_owned(),
..QueueEvent::default()
}];
let lidarr_serdeable: LidarrSerdeable = queue_events.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::QueueEvents(queue_events));
}
#[test]
fn test_lidarr_serdeable_from_root_folders() {
let root_folders = vec![RootFolder {
@@ -458,6 +541,30 @@ mod tests {
assert_eq!(lidarr_serdeable, LidarrSerdeable::Album(album));
}
#[test]
fn test_lidarr_serdeable_from_tasks() {
let tasks = vec![LidarrTask {
name: "test".to_owned(),
..LidarrTask::default()
}];
let lidarr_serdeable: LidarrSerdeable = tasks.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Tasks(tasks));
}
#[test]
fn test_lidarr_serdeable_from_updates() {
let updates = vec![Update {
version: "test".to_owned(),
..Update::default()
}];
let lidarr_serdeable: LidarrSerdeable = updates.clone().into();
assert_eq!(lidarr_serdeable, LidarrSerdeable::Updates(updates));
}
#[test]
fn test_artist_status_display() {
assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing");
+237 -13
View File
@@ -2,15 +2,21 @@ use serde_json::Number;
use super::modals::{AddArtistModal, AddRootFolderModal, EditArtistModal};
use crate::app::context_clues::{
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
use crate::models::lidarr_models::LidarrTask;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::{IndexerSettings, QueueEvent};
use crate::models::stateful_list::StatefulList;
use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState,
BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState,
lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord, LidarrHistoryItem},
servarr_models::{DiskSpace, RootFolder},
servarr_data::modals::IndexerTestResultModalItem,
servarr_models::{DiskSpace, Indexer, RootFolder},
stateful_table::StatefulTable,
};
use crate::network::lidarr_network::LidarrEvent;
@@ -22,12 +28,18 @@ use strum::EnumIter;
use {
crate::models::lidarr_models::{MonitorType, NewItemMonitorType},
crate::models::stateful_table::SortOption,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map,
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
add_artist_search_result, album, artist, download_record, lidarr_history_item,
add_artist_search_result, album, artist, download_record, indexer, lidarr_history_item,
metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map,
},
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{log_line, task},
crate::network::servarr_test_utils::diskspace,
crate::network::servarr_test_utils::indexer_test_result,
crate::network::servarr_test_utils::queued_event,
crate::network::sonarr_network::sonarr_network_test_utils::test_utils::updates,
crate::sort_option,
strum::{Display, EnumString, IntoEnumIterator},
};
@@ -42,22 +54,33 @@ pub struct LidarrData<'a> {
pub add_root_folder_modal: Option<AddRootFolderModal>,
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
pub albums: StatefulTable<Album>,
pub artist_history: Option<StatefulTable<LidarrHistoryItem>>,
pub artist_info_tabs: TabState,
pub artists: StatefulTable<Artist>,
pub delete_files: bool,
pub disk_space_vec: Vec<DiskSpace>,
pub downloads: StatefulTable<DownloadRecord>,
pub edit_artist_modal: Option<EditArtistModal>,
pub edit_indexer_modal: Option<EditIndexerModal>,
pub history: StatefulTable<LidarrHistoryItem>,
pub indexers: StatefulTable<Indexer>,
pub indexer_settings: Option<IndexerSettings>,
pub indexer_test_all_results: Option<StatefulTable<IndexerTestResultModalItem>>,
pub indexer_test_errors: Option<String>,
pub logs: StatefulList<HorizontallyScrollableText>,
pub log_details: StatefulList<HorizontallyScrollableText>,
pub main_tabs: TabState,
pub metadata_profile_map: BiMap<i64, String>,
pub prompt_confirm: bool,
pub prompt_confirm_action: Option<LidarrEvent>,
pub quality_profile_map: BiMap<i64, String>,
pub queued_events: StatefulTable<QueueEvent>,
pub root_folders: StatefulTable<RootFolder>,
pub selected_block: BlockSelectionState<'a, ActiveLidarrBlock>,
pub start_time: DateTime<Utc>,
pub tags_map: BiMap<i64, String>,
pub tasks: StatefulTable<LidarrTask>,
pub updates: ScrollableText,
pub version: String,
}
@@ -69,6 +92,7 @@ impl LidarrData<'_> {
pub fn reset_artist_info_tabs(&mut self) {
self.albums = StatefulTable::default();
self.artist_history = None;
self.artist_info_tabs.index = 0;
}
@@ -113,20 +137,31 @@ impl<'a> Default for LidarrData<'a> {
add_root_folder_modal: None,
add_searched_artists: None,
albums: StatefulTable::default(),
artist_history: None,
artists: StatefulTable::default(),
delete_files: false,
disk_space_vec: Vec::new(),
downloads: StatefulTable::default(),
edit_artist_modal: None,
edit_indexer_modal: None,
history: StatefulTable::default(),
indexers: StatefulTable::default(),
indexer_settings: None,
indexer_test_all_results: None,
indexer_test_errors: None,
logs: StatefulList::default(),
log_details: StatefulList::default(),
metadata_profile_map: BiMap::new(),
prompt_confirm: false,
prompt_confirm_action: None,
quality_profile_map: BiMap::new(),
queued_events: StatefulTable::default(),
root_folders: StatefulTable::default(),
selected_block: BlockSelectionState::default(),
start_time: DateTime::default(),
tags_map: BiMap::new(),
tasks: StatefulTable::default(),
updates: ScrollableText::default(),
version: String::new(),
main_tabs: TabState::new(vec![
TabRoute {
@@ -153,13 +188,33 @@ impl<'a> Default for LidarrData<'a> {
contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "Indexers".to_string(),
route: ActiveLidarrBlock::Indexers.into(),
contextual_help: Some(&INDEXERS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "System".to_string(),
route: ActiveLidarrBlock::System.into(),
contextual_help: Some(&SYSTEM_CONTEXT_CLUES),
config: None,
},
]),
artist_info_tabs: TabState::new(vec![
TabRoute {
title: "Albums".to_string(),
route: ActiveLidarrBlock::ArtistDetails.into(),
contextual_help: Some(&ARTIST_DETAILS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveLidarrBlock::ArtistHistory.into(),
contextual_help: Some(&ARTIST_HISTORY_CONTEXT_CLUES),
config: None,
},
]),
artist_info_tabs: TabState::new(vec![TabRoute {
title: "Albums".to_string(),
route: ActiveLidarrBlock::ArtistDetails.into(),
contextual_help: Some(&ARTIST_DETAILS_CONTEXT_CLUES),
config: None,
}]),
}
}
}
@@ -221,15 +276,44 @@ impl LidarrData<'_> {
.metadata_profile_list
.set_items(vec![metadata_profile().name]);
let edit_indexer_modal = EditIndexerModal {
name: "DrunkenSlug".into(),
enable_rss: Some(true),
enable_automatic_search: Some(true),
enable_interactive_search: Some(true),
url: "http://127.0.0.1:9696/1/".into(),
api_key: "someApiKey".into(),
seed_ratio: "ratio".into(),
tags: "25".into(),
priority: 1,
};
let mut indexer_test_all_results = StatefulTable::default();
indexer_test_all_results.set_items(vec![indexer_test_result()]);
let mut artist_history = StatefulTable::default();
artist_history.set_items(vec![lidarr_history_item()]);
artist_history.sorting(vec![sort_option!(id)]);
artist_history.search = Some("artist history search".into());
artist_history.filter = Some("artist history filter".into());
let mut lidarr_data = LidarrData {
artist_history: Some(artist_history),
delete_files: true,
disk_space_vec: vec![diskspace()],
quality_profile_map: quality_profile_map(),
metadata_profile_map: metadata_profile_map(),
edit_artist_modal: Some(edit_artist_modal),
edit_indexer_modal: Some(edit_indexer_modal),
add_root_folder_modal: Some(add_root_folder_modal),
add_artist_modal: Some(add_artist_modal),
indexer_settings: Some(indexer_settings()),
indexer_test_all_results: Some(indexer_test_all_results),
indexer_test_errors: Some("error".to_string()),
start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()),
tags_map: tags_map(),
updates: updates(),
version: "1.2.3.4".to_owned(),
..LidarrData::default()
};
lidarr_data.albums.set_items(vec![album()]);
@@ -250,11 +334,15 @@ impl LidarrData<'_> {
lidarr_data.history.search = Some("test search".into());
lidarr_data.history.filter = Some("test filter".into());
lidarr_data.root_folders.set_items(vec![root_folder()]);
lidarr_data.version = "1.0.0".to_owned();
lidarr_data.indexers.set_items(vec![indexer()]);
lidarr_data.queued_events.set_items(vec![queued_event()]);
lidarr_data.add_artist_search = Some("Test Artist".into());
let mut add_searched_artists = StatefulTable::default();
add_searched_artists.set_items(vec![add_artist_search_result()]);
lidarr_data.add_searched_artists = Some(add_searched_artists);
lidarr_data.logs.set_items(vec![log_line().into()]);
lidarr_data.log_details.set_items(vec![log_line().into()]);
lidarr_data.tasks.set_items(vec![task()]);
lidarr_data
}
@@ -266,6 +354,9 @@ pub enum ActiveLidarrBlock {
#[default]
Artists,
ArtistDetails,
ArtistHistory,
ArtistHistoryDetails,
ArtistHistorySortPrompt,
ArtistsSortPrompt,
AddArtistAlreadyInLibrary,
AddArtistConfirmPrompt,
@@ -288,6 +379,7 @@ pub enum ActiveLidarrBlock {
AddRootFolderSelectQualityProfile,
AddRootFolderSelectMetadataProfile,
AddRootFolderTagsInput,
AllIndexerSettingsPrompt,
AutomaticallySearchArtistPrompt,
DeleteAlbumPrompt,
DeleteAlbumConfirmPrompt,
@@ -308,13 +400,35 @@ pub enum ActiveLidarrBlock {
EditArtistSelectQualityProfile,
EditArtistTagsInput,
EditArtistToggleMonitored,
EditIndexerPrompt,
EditIndexerConfirmPrompt,
EditIndexerApiKeyInput,
EditIndexerNameInput,
EditIndexerSeedRatioInput,
EditIndexerToggleEnableRss,
EditIndexerToggleEnableAutomaticSearch,
EditIndexerToggleEnableInteractiveSearch,
EditIndexerUrlInput,
EditIndexerPriorityInput,
EditIndexerTagsInput,
DeleteIndexerPrompt,
FilterArtists,
FilterArtistsError,
FilterHistory,
FilterHistoryError,
FilterArtistHistory,
FilterArtistHistoryError,
History,
HistoryItemDetails,
HistorySortPrompt,
Indexers,
IndexerSettingsConfirmPrompt,
IndexerSettingsMaximumSizeInput,
IndexerSettingsMinimumAgeInput,
IndexerSettingsRetentionInput,
IndexerSettingsRssSyncIntervalInput,
TestAllIndexers,
TestIndexer,
RootFolders,
SearchAlbums,
SearchAlbumsError,
@@ -322,6 +436,14 @@ pub enum ActiveLidarrBlock {
SearchArtistsError,
SearchHistory,
SearchHistoryError,
SearchArtistHistory,
SearchArtistHistoryError,
System,
SystemLogs,
SystemQueuedEvents,
SystemTasks,
SystemTaskStartConfirmPrompt,
SystemUpdates,
UpdateAllArtistsPrompt,
UpdateAndScanArtistPrompt,
UpdateDownloadsPrompt,
@@ -337,11 +459,18 @@ pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::UpdateAllArtistsPrompt,
];
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 12] = [
ActiveLidarrBlock::ArtistDetails,
ActiveLidarrBlock::ArtistHistory,
ActiveLidarrBlock::ArtistHistoryDetails,
ActiveLidarrBlock::ArtistHistorySortPrompt,
ActiveLidarrBlock::AutomaticallySearchArtistPrompt,
ActiveLidarrBlock::FilterArtistHistory,
ActiveLidarrBlock::FilterArtistHistoryError,
ActiveLidarrBlock::SearchAlbums,
ActiveLidarrBlock::SearchAlbumsError,
ActiveLidarrBlock::SearchArtistHistory,
ActiveLidarrBlock::SearchArtistHistoryError,
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
];
@@ -461,6 +590,101 @@ pub const ADD_ROOT_FOLDER_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::AddRootFolderConfirmPrompt],
];
pub static EDIT_INDEXER_BLOCKS: [ActiveLidarrBlock; 11] = [
ActiveLidarrBlock::EditIndexerPrompt,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
ActiveLidarrBlock::EditIndexerApiKeyInput,
ActiveLidarrBlock::EditIndexerNameInput,
ActiveLidarrBlock::EditIndexerSeedRatioInput,
ActiveLidarrBlock::EditIndexerToggleEnableRss,
ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveLidarrBlock::EditIndexerPriorityInput,
ActiveLidarrBlock::EditIndexerUrlInput,
ActiveLidarrBlock::EditIndexerTagsInput,
];
pub const EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[
ActiveLidarrBlock::EditIndexerNameInput,
ActiveLidarrBlock::EditIndexerUrlInput,
],
&[
ActiveLidarrBlock::EditIndexerToggleEnableRss,
ActiveLidarrBlock::EditIndexerApiKeyInput,
],
&[
ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveLidarrBlock::EditIndexerSeedRatioInput,
],
&[
ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveLidarrBlock::EditIndexerTagsInput,
],
&[
ActiveLidarrBlock::EditIndexerPriorityInput,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
],
&[
ActiveLidarrBlock::EditIndexerConfirmPrompt,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
],
];
pub const EDIT_INDEXER_NZB_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[
ActiveLidarrBlock::EditIndexerNameInput,
ActiveLidarrBlock::EditIndexerUrlInput,
],
&[
ActiveLidarrBlock::EditIndexerToggleEnableRss,
ActiveLidarrBlock::EditIndexerApiKeyInput,
],
&[
ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveLidarrBlock::EditIndexerTagsInput,
],
&[
ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveLidarrBlock::EditIndexerPriorityInput,
],
&[
ActiveLidarrBlock::EditIndexerConfirmPrompt,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
],
];
pub static INDEXER_SETTINGS_BLOCKS: [ActiveLidarrBlock; 6] = [
ActiveLidarrBlock::AllIndexerSettingsPrompt,
ActiveLidarrBlock::IndexerSettingsConfirmPrompt,
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,
ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,
ActiveLidarrBlock::IndexerSettingsRetentionInput,
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput,
];
pub const INDEXER_SETTINGS_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::IndexerSettingsMinimumAgeInput],
&[ActiveLidarrBlock::IndexerSettingsRetentionInput],
&[ActiveLidarrBlock::IndexerSettingsMaximumSizeInput],
&[ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput],
&[ActiveLidarrBlock::IndexerSettingsConfirmPrompt],
];
pub static INDEXERS_BLOCKS: [ActiveLidarrBlock; 3] = [
ActiveLidarrBlock::Indexers,
ActiveLidarrBlock::DeleteIndexerPrompt,
ActiveLidarrBlock::TestIndexer,
];
pub static SYSTEM_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
ActiveLidarrBlock::SystemLogs,
ActiveLidarrBlock::SystemQueuedEvents,
ActiveLidarrBlock::SystemTasks,
ActiveLidarrBlock::SystemTaskStartConfirmPrompt,
ActiveLidarrBlock::SystemUpdates,
];
impl From<ActiveLidarrBlock> for Route {
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
Route::Lidarr(active_lidarr_block, None)
@@ -1,18 +1,22 @@
#[cfg(test)]
mod tests {
use crate::app::context_clues::{
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
use crate::models::lidarr_models::Album;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS,
DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_BLOCKS,
ROOT_FOLDERS_BLOCKS,
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ARTIST_DETAILS_BLOCKS,
DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS,
DELETE_ARTIST_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS,
EDIT_ARTIST_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
use crate::models::{
BlockSelectionState, Route,
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS, LidarrData},
@@ -56,11 +60,13 @@ mod tests {
fn test_reset_artist_info_tabs() {
let mut lidarr_data = LidarrData::default();
lidarr_data.albums.set_items(vec![Album::default()]);
lidarr_data.artist_history = Some(StatefulTable::default());
lidarr_data.artist_info_tabs.index = 1;
lidarr_data.reset_artist_info_tabs();
assert_is_empty!(lidarr_data.albums);
assert_none!(lidarr_data.artist_history);
assert_eq!(lidarr_data.artist_info_tabs.index, 0);
}
@@ -134,23 +140,29 @@ mod tests {
assert_none!(lidarr_data.add_searched_artists);
assert_is_empty!(lidarr_data.albums);
assert_is_empty!(lidarr_data.artists);
assert_none!(lidarr_data.artist_history);
assert!(!lidarr_data.delete_files);
assert_is_empty!(lidarr_data.disk_space_vec);
assert_is_empty!(lidarr_data.downloads);
assert_none!(lidarr_data.edit_artist_modal);
assert_none!(lidarr_data.add_root_folder_modal);
assert_is_empty!(lidarr_data.history);
assert_is_empty!(lidarr_data.logs);
assert_is_empty!(lidarr_data.log_details);
assert_is_empty!(lidarr_data.metadata_profile_map);
assert!(!lidarr_data.prompt_confirm);
assert_none!(lidarr_data.prompt_confirm_action);
assert_is_empty!(lidarr_data.quality_profile_map);
assert_is_empty!(lidarr_data.queued_events);
assert_is_empty!(lidarr_data.root_folders);
assert_eq!(lidarr_data.selected_block, BlockSelectionState::default());
assert_eq!(lidarr_data.start_time, <DateTime<Utc>>::default());
assert_is_empty!(lidarr_data.tags_map);
assert_is_empty!(lidarr_data.tasks);
assert_is_empty!(lidarr_data.updates);
assert_is_empty!(lidarr_data.version);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 4);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 6);
assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
@@ -196,7 +208,29 @@ mod tests {
);
assert_none!(lidarr_data.main_tabs.tabs[3].config);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1);
assert_str_eq!(lidarr_data.main_tabs.tabs[4].title, "Indexers");
assert_eq!(
lidarr_data.main_tabs.tabs[4].route,
ActiveLidarrBlock::Indexers.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[4].contextual_help,
&INDEXERS_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[4].config);
assert_str_eq!(lidarr_data.main_tabs.tabs[5].title, "System");
assert_eq!(
lidarr_data.main_tabs.tabs[5].route,
ActiveLidarrBlock::System.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[5].contextual_help,
&SYSTEM_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[5].config);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 2);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
assert_eq!(
lidarr_data.artist_info_tabs.tabs[0].route,
@@ -207,6 +241,17 @@ mod tests {
&ARTIST_DETAILS_CONTEXT_CLUES
);
assert_none!(lidarr_data.artist_info_tabs.tabs[0].config);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[1].title, "History");
assert_eq!(
lidarr_data.artist_info_tabs.tabs[1].route,
ActiveLidarrBlock::ArtistHistory.into()
);
assert_some_eq_x!(
&lidarr_data.artist_info_tabs.tabs[1].contextual_help,
&ARTIST_HISTORY_CONTEXT_CLUES
);
assert_none!(lidarr_data.artist_info_tabs.tabs[1].config);
}
#[test]
@@ -223,11 +268,18 @@ mod tests {
#[test]
fn test_artist_details_blocks_contains_expected_blocks() {
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 5);
assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 12);
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistDetails));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistory));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistoryDetails));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistorySortPrompt));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchArtistPrompt));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistHistory));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistHistoryError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbums));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumsError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistHistory));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistHistoryError));
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
}
@@ -414,9 +466,168 @@ mod tests {
assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::DeleteRootFolderPrompt));
}
#[test]
fn test_edit_indexer_blocks_contents() {
assert_eq!(EDIT_INDEXER_BLOCKS.len(), 11);
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerPrompt));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerConfirmPrompt));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerApiKeyInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerNameInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerSeedRatioInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableRss));
assert!(
EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch)
);
assert!(
EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch)
);
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerUrlInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerTagsInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerPriorityInput));
}
#[test]
fn test_edit_indexer_nzb_selection_blocks_ordering() {
let mut edit_indexer_nzb_selection_block_iter = EDIT_INDEXER_NZB_SELECTION_BLOCKS.iter();
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerNameInput,
ActiveLidarrBlock::EditIndexerUrlInput,
]
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerToggleEnableRss,
ActiveLidarrBlock::EditIndexerApiKeyInput,
]
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveLidarrBlock::EditIndexerTagsInput,
]
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveLidarrBlock::EditIndexerPriorityInput,
]
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerConfirmPrompt,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
]
);
assert_eq!(edit_indexer_nzb_selection_block_iter.next(), None);
}
#[test]
fn test_edit_indexer_torrent_selection_blocks_ordering() {
let mut edit_indexer_torrent_selection_block_iter =
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.iter();
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerNameInput,
ActiveLidarrBlock::EditIndexerUrlInput,
]
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerToggleEnableRss,
ActiveLidarrBlock::EditIndexerApiKeyInput,
]
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveLidarrBlock::EditIndexerSeedRatioInput,
]
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveLidarrBlock::EditIndexerTagsInput,
]
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerPriorityInput,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
]
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&[
ActiveLidarrBlock::EditIndexerConfirmPrompt,
ActiveLidarrBlock::EditIndexerConfirmPrompt,
]
);
assert_eq!(edit_indexer_torrent_selection_block_iter.next(), None);
}
#[test]
fn test_indexer_settings_blocks_contents() {
assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 6);
assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::AllIndexerSettingsPrompt));
assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsConfirmPrompt));
assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsMaximumSizeInput));
assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsMinimumAgeInput));
assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsRetentionInput));
assert!(
INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput)
);
}
#[test]
fn test_indexer_settings_selection_blocks_ordering() {
let mut indexer_settings_block_iter = INDEXER_SETTINGS_SELECTION_BLOCKS.iter();
assert_eq!(
indexer_settings_block_iter.next().unwrap(),
&[ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,]
);
assert_eq!(
indexer_settings_block_iter.next().unwrap(),
&[ActiveLidarrBlock::IndexerSettingsRetentionInput,]
);
assert_eq!(
indexer_settings_block_iter.next().unwrap(),
&[ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,]
);
assert_eq!(
indexer_settings_block_iter.next().unwrap(),
&[ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput,]
);
assert_eq!(
indexer_settings_block_iter.next().unwrap(),
&[ActiveLidarrBlock::IndexerSettingsConfirmPrompt,]
);
assert_eq!(indexer_settings_block_iter.next(), None);
}
#[test]
fn test_indexers_blocks_contents() {
assert_eq!(INDEXERS_BLOCKS.len(), 3);
assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::Indexers));
assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::DeleteIndexerPrompt));
assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::TestIndexer));
}
#[test]
fn test_add_root_folder_blocks_contents() {
use crate::models::servarr_data::lidarr::lidarr_data::ADD_ROOT_FOLDER_BLOCKS;
assert_eq!(ADD_ROOT_FOLDER_BLOCKS.len(), 9);
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderPrompt));
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderConfirmPrompt));
@@ -432,4 +643,14 @@ mod tests {
);
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderTagsInput));
}
#[test]
fn test_system_details_blocks_contents() {
assert_eq!(SYSTEM_DETAILS_BLOCKS.len(), 5);
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemLogs));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemQueuedEvents));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTasks));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTaskStartConfirmPrompt));
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemUpdates));
}
}
+72
View File
@@ -1,6 +1,8 @@
use strum::IntoEnumIterator;
use super::lidarr_data::LidarrData;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::Indexer;
use crate::models::{
HorizontallyScrollableText,
lidarr_models::{MonitorType, NewItemMonitorType},
@@ -114,6 +116,76 @@ impl From<&LidarrData<'_>> for EditArtistModal {
}
}
impl From<&LidarrData<'_>> for EditIndexerModal {
fn from(lidarr_data: &LidarrData<'_>) -> EditIndexerModal {
let mut edit_indexer_modal = EditIndexerModal::default();
let Indexer {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
tags,
fields,
priority,
..
} = lidarr_data.indexers.current_selection();
let seed_ratio_field_option = fields
.as_ref()
.expect("indexer fields must exist")
.iter()
.find(|field| {
field.name.as_ref().expect("indexer field name must exist") == "seedCriteria.seedRatio"
});
let seed_ratio_value_option = if let Some(seed_ratio_field) = seed_ratio_field_option {
seed_ratio_field.value.clone()
} else {
None
};
edit_indexer_modal.name = name.clone().expect("indexer name must exist").into();
edit_indexer_modal.enable_rss = Some(*enable_rss);
edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search);
edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search);
edit_indexer_modal.priority = *priority;
edit_indexer_modal.url = fields
.as_ref()
.expect("indexer fields must exist")
.iter()
.find(|field| field.name.as_ref().expect("indexer field name must exist") == "baseUrl")
.expect("baseUrl field must exist")
.value
.clone()
.expect("baseUrl field value must exist")
.as_str()
.expect("baseUrl field value must be a string")
.into();
edit_indexer_modal.api_key = fields
.as_ref()
.expect("indexer fields must exist")
.iter()
.find(|field| field.name.as_ref().expect("indexer field name must exist") == "apiKey")
.expect("apiKey field must exist")
.value
.clone()
.expect("apiKey field value must exist")
.as_str()
.expect("apiKey field value must be a string")
.into();
if let Some(seed_ratio_value) = seed_ratio_value_option {
edit_indexer_modal.seed_ratio = seed_ratio_value
.as_f64()
.expect("Seed ratio value must be a valid f64")
.to_string()
.into();
}
edit_indexer_modal.tags = lidarr_data.tag_ids_to_display(tags).into();
edit_indexer_modal
}
}
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct AddRootFolderModal {
+104 -4
View File
@@ -1,12 +1,14 @@
#[cfg(test)]
mod tests {
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType};
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
use crate::models::servarr_data::lidarr::modals::{AddArtistModal, EditArtistModal};
use crate::models::servarr_models::RootFolder;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::{Number, Value};
#[test]
fn test_add_artist_modal_from_lidarr_data() {
@@ -108,4 +110,102 @@ mod tests {
assert_str_eq!(edit_artist_modal.path.text, "/nfs/music/test_artist");
assert_str_eq!(edit_artist_modal.tags.text, "usenet");
}
#[rstest]
fn test_edit_indexer_modal_from_lidarr_data(#[values(true, false)] seed_ratio_present: bool) {
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 seed_ratio_present {
fields.push(IndexerField {
name: Some("seedCriteria.seedRatio".to_owned()),
value: Some(Value::from(1.2f64)),
});
}
let indexer = Indexer {
name: Some("Test".to_owned()),
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
tags: vec![Number::from(1), Number::from(2)],
fields: Some(fields),
priority: 1,
..Indexer::default()
};
lidarr_data.indexers.set_items(vec![indexer]);
let edit_indexer_modal = EditIndexerModal::from(&lidarr_data);
assert_str_eq!(edit_indexer_modal.name.text, "Test");
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
assert_eq!(edit_indexer_modal.priority, 1);
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
if seed_ratio_present {
assert_str_eq!(edit_indexer_modal.seed_ratio.text, "1.2");
} else {
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
}
}
#[test]
fn test_edit_indexer_modal_from_lidarr_data_seed_ratio_value_is_none() {
let mut lidarr_data = LidarrData {
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
..LidarrData::default()
};
let 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())),
},
IndexerField {
name: Some("seedCriteria.seedRatio".to_owned()),
value: None,
},
];
let indexer = Indexer {
name: Some("Test".to_owned()),
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
tags: vec![Number::from(1), Number::from(2)],
fields: Some(fields),
priority: 1,
..Indexer::default()
};
lidarr_data.indexers.set_items(vec![indexer]);
let edit_indexer_modal = EditIndexerModal::from(&lidarr_data);
assert_str_eq!(edit_indexer_modal.name.text, "Test");
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
assert_eq!(edit_indexer_modal.priority, 1);
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
}
}
+21 -1
View File
@@ -1,6 +1,10 @@
use crate::models::HorizontallyScrollableText;
#[derive(Default, Debug, PartialEq, Eq)]
#[cfg(test)]
#[path = "modals_tests.rs"]
mod modals_tests;
#[derive(Debug, PartialEq, Eq)]
pub struct EditIndexerModal {
pub name: HorizontallyScrollableText,
pub enable_rss: Option<bool>,
@@ -13,6 +17,22 @@ pub struct EditIndexerModal {
pub priority: i64,
}
impl Default for EditIndexerModal {
fn default() -> Self {
Self {
name: Default::default(),
enable_rss: None,
enable_automatic_search: None,
enable_interactive_search: None,
url: Default::default(),
api_key: Default::default(),
seed_ratio: Default::default(),
tags: Default::default(),
priority: 1,
}
}
}
#[derive(Default, Clone, Eq, PartialEq, Debug)]
pub struct IndexerTestResultModalItem {
pub name: String,
+20
View File
@@ -0,0 +1,20 @@
#[cfg(test)]
mod tests {
use crate::models::servarr_data::modals::EditIndexerModal;
use pretty_assertions::assert_eq;
#[test]
fn test_edit_indexer_modal_default() {
let edit_indexer_modal = EditIndexerModal::default();
assert_is_empty!(edit_indexer_modal.name.text);
assert_none!(&edit_indexer_modal.enable_rss);
assert_none!(&edit_indexer_modal.enable_automatic_search);
assert_none!(&edit_indexer_modal.enable_interactive_search);
assert_is_empty!(edit_indexer_modal.url.text);
assert_is_empty!(edit_indexer_modal.api_key.text);
assert_is_empty!(edit_indexer_modal.seed_ratio.text);
assert_is_empty!(edit_indexer_modal.tags.text);
assert_eq!(edit_indexer_modal.priority, 1);
}
}
+10 -9
View File
@@ -12,10 +12,10 @@ use crate::{
models::{
BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState,
servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem},
servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder},
servarr_models::{DiskSpace, Indexer, IndexerSettings, QueueEvent, RootFolder},
sonarr_models::{
AddSeriesSearchResult, BlocklistItem, DownloadRecord, IndexerSettings, Season, Series,
SonarrHistoryItem, SonarrTask,
AddSeriesSearchResult, BlocklistItem, DownloadRecord, Season, Series, SonarrHistoryItem,
SonarrTask,
},
stateful_list::StatefulList,
stateful_table::StatefulTable,
@@ -33,11 +33,12 @@ use {
crate::models::sonarr_models::{SeriesMonitor, SeriesType},
crate::models::stateful_table::SortOption,
crate::network::servarr_test_utils::diskspace,
crate::network::servarr_test_utils::indexer_settings,
crate::network::servarr_test_utils::indexer_test_result,
crate::network::servarr_test_utils::queued_event,
crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
add_series_search_result, blocklist_item, download_record, history_item, indexer,
indexer_settings, log_line, root_folder,
add_series_search_result, blocklist_item, download_record, indexer, log_line, root_folder,
sonarr_history_item,
},
crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
episode, episode_file, language_profiles_map, quality_profile_map, season, series, tags_map,
@@ -308,7 +309,7 @@ impl SonarrData<'_> {
};
episode_details_modal
.episode_history
.set_items(vec![history_item()]);
.set_items(vec![sonarr_history_item()]);
episode_details_modal
.episode_releases
.set_items(vec![torrent_release(), usenet_release()]);
@@ -327,7 +328,7 @@ impl SonarrData<'_> {
.set_items(vec![episode_file()]);
season_details_modal
.season_history
.set_items(vec![history_item()]);
.set_items(vec![sonarr_history_item()]);
season_details_modal.season_history.search = Some("season history search".into());
season_details_modal.season_history.filter = Some("season history filter".into());
season_details_modal
@@ -341,7 +342,7 @@ impl SonarrData<'_> {
.sorting(vec![sort_option!(indexer_id)]);
let mut series_history = StatefulTable::default();
series_history.set_items(vec![history_item()]);
series_history.set_items(vec![sonarr_history_item()]);
series_history.sorting(vec![sort_option!(id)]);
series_history.search = Some("series history search".into());
series_history.filter = Some("series history filter".into());
@@ -373,7 +374,7 @@ impl SonarrData<'_> {
sonarr_data.blocklist.set_items(vec![blocklist_item()]);
sonarr_data.blocklist.sorting(vec![sort_option!(id)]);
sonarr_data.downloads.set_items(vec![download_record()]);
sonarr_data.history.set_items(vec![history_item()]);
sonarr_data.history.set_items(vec![sonarr_history_item()]);
sonarr_data.history.sorting(vec![sort_option!(id)]);
sonarr_data.history.search = Some("test search".into());
sonarr_data.history.filter = Some("test filter".into());
+38 -1
View File
@@ -89,6 +89,21 @@ pub struct DiskSpace {
pub total_space: i64,
}
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IndexerSettings {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub minimum_age: i64,
#[serde(deserialize_with = "super::from_i64")]
pub retention: i64,
#[serde(deserialize_with = "super::from_i64")]
pub maximum_size: i64,
#[serde(deserialize_with = "super::from_i64")]
pub rss_sync_interval: i64,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditIndexerParams {
@@ -130,7 +145,7 @@ pub struct HostConfig {
pub ssl_cert_password: Option<String>,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Indexer {
#[serde(deserialize_with = "super::from_i64")]
@@ -153,6 +168,28 @@ pub struct Indexer {
pub tags: Vec<Number>,
}
impl Default for Indexer {
fn default() -> Self {
Self {
id: 0,
name: None,
implementation: None,
implementation_name: None,
config_contract: None,
supports_rss: false,
supports_search: false,
fields: None,
enable_rss: false,
enable_automatic_search: false,
enable_interactive_search: false,
protocol: "".to_string(),
priority: 1,
download_client_id: 0,
tags: vec![],
}
}
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct IndexerTestResult {
+22 -1
View File
@@ -3,9 +3,30 @@ mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::models::servarr_models::{
AuthenticationMethod, AuthenticationRequired, CertificateValidation, QualityProfile,
AuthenticationMethod, AuthenticationRequired, CertificateValidation, Indexer, QualityProfile,
};
#[test]
fn test_indexer_default() {
let indexer = Indexer::default();
assert_eq!(indexer.id, 0);
assert_none!(indexer.name);
assert_none!(indexer.implementation);
assert_none!(indexer.implementation_name);
assert_none!(indexer.config_contract);
assert!(!indexer.supports_rss);
assert!(!indexer.supports_search);
assert_none!(indexer.fields);
assert!(!indexer.enable_rss);
assert!(!indexer.enable_automatic_search);
assert!(!indexer.enable_interactive_search);
assert_is_empty!(indexer.protocol);
assert_eq!(indexer.priority, 1);
assert_eq!(indexer.download_client_id, 0);
assert_is_empty!(indexer.tags);
}
#[test]
fn test_authentication_method_display() {
assert_str_eq!(AuthenticationMethod::Basic.to_string(), "basic");
+4 -16
View File
@@ -1,6 +1,9 @@
use std::fmt::{Display, Formatter};
use crate::{models::servarr_models::IndexerTestResult, serde_enum_from};
use crate::{
models::servarr_models::{IndexerSettings, IndexerTestResult},
serde_enum_from,
};
use chrono::{DateTime, Utc};
use clap::ValueEnum;
use derivative::Derivative;
@@ -221,21 +224,6 @@ pub struct EpisodeFile {
pub media_info: Option<MediaInfo>,
}
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IndexerSettings {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub minimum_age: i64,
#[serde(deserialize_with = "super::from_i64")]
pub retention: i64,
#[serde(deserialize_with = "super::from_i64")]
pub maximum_size: i64,
#[serde(deserialize_with = "super::from_i64")]
pub rss_sync_interval: i64,
}
#[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
+5 -5
View File
@@ -6,14 +6,14 @@ mod tests {
use crate::models::{
Serdeable,
servarr_models::{
DiskSpace, HostConfig, Indexer, IndexerTestResult, Language, Log, LogResponse,
QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Language, Log,
LogResponse, QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
},
sonarr_models::{
AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadStatus,
DownloadsResponse, Episode, EpisodeFile, IndexerSettings, Series, SeriesMonitor,
SeriesStatus, SeriesType, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease,
SonarrSerdeable, SonarrTask, SonarrTaskName, SystemStatus,
DownloadsResponse, Episode, EpisodeFile, Series, SeriesMonitor, SeriesStatus, SeriesType,
SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask,
SonarrTaskName, SystemStatus,
},
};
@@ -95,7 +95,7 @@ mod tests {
mock.assert_async().await;
assert!(result.is_ok());
let LidarrSerdeable::HistoryWrapper(history) = result.unwrap() else {
let LidarrSerdeable::LidarrHistoryWrapper(history) = result.unwrap() else {
panic!("Expected LidarrHistoryWrapper")
};
assert_eq!(
@@ -165,7 +165,7 @@ mod tests {
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::HistoryWrapper(history) = network
let LidarrSerdeable::LidarrHistoryWrapper(history) = network
.handle_lidarr_event(LidarrEvent::GetHistory(500))
.await
.unwrap()
@@ -0,0 +1,901 @@
#[cfg(test)]
mod tests {
use crate::models::HorizontallyScrollableText;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult};
use crate::network::NetworkResource;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
indexer, indexer_settings,
};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use bimap::BiMap;
use mockito::Matcher;
use pretty_assertions::assert_eq;
use serde_json::json;
#[tokio::test]
async fn test_handle_delete_lidarr_indexer_event() {
let (mock, app, _server) = MockServarrApi::delete()
.path("/1")
.build_for(LidarrEvent::DeleteIndexer(1))
.await;
app
.lock()
.await
.data
.lidarr_data
.indexers
.set_items(vec![indexer()]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::DeleteIndexer(1))
.await
.is_ok()
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_all_indexer_settings_event() {
let indexer_settings_json = json!({
"id": 1,
"minimumAge": 1,
"maximumSize": 12345,
"retention": 1,
"rssSyncInterval": 60
});
let (mock, app, _server) = MockServarrApi::put()
.with_request_body(indexer_settings_json)
.build_for(LidarrEvent::EditAllIndexerSettings(indexer_settings()))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::EditAllIndexerSettings(indexer_settings()))
.await
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_lidarr_indexer_event() {
let expected_edit_indexer_params = EditIndexerParams {
indexer_id: 1,
name: Some("Test Update".to_owned()),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: Some("https://localhost:9696/1/".to_owned()),
api_key: Some("test1234".to_owned()),
seed_ratio: Some("1.3".to_owned()),
tag_input_string: Some("usenet, testing".to_owned()),
priority: Some(0),
..EditIndexerParams::default()
};
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"priority": 0,
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.3",
},
],
"tags": [1, 2],
"id": 1
});
let (mock_details_server, app, mut server) = MockServarrApi::get()
.returns(indexer_details_json)
.path("/1")
.build_for(LidarrEvent::GetIndexers)
.await;
let mock_edit_server = server
.mock(
"PUT",
format!(
"/api/v1{}/1?forceSave=true",
LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource()
)
.as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
app.lock().await.data.lidarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params))
.await
);
mock_details_server.assert_async().await;
mock_edit_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_lidarr_indexer_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none()
{
let expected_edit_indexer_params = EditIndexerParams {
indexer_id: 1,
name: Some("Test Update".to_owned()),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: Some("https://localhost:9696/1/".to_owned()),
api_key: Some("test1234".to_owned()),
seed_ratio: Some("1.3".to_owned()),
tags: Some(vec![1, 2]),
priority: Some(0),
..EditIndexerParams::default()
};
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"priority": 0,
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.3",
},
],
"tags": [1, 2],
"id": 1
});
let (mock_details_server, app, mut server) = MockServarrApi::get()
.returns(indexer_details_json)
.path("/1")
.build_for(LidarrEvent::GetIndexers)
.await;
let mock_edit_server = server
.mock(
"PUT",
format!(
"/api/v1{}/1?forceSave=true",
LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource()
)
.as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params))
.await
);
mock_details_server.assert_async().await;
mock_edit_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_lidarr_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details()
{
let expected_edit_indexer_params = EditIndexerParams {
indexer_id: 1,
name: Some("Test Update".to_owned()),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: Some("https://localhost:9696/1/".to_owned()),
api_key: Some("test1234".to_owned()),
seed_ratio: Some("1.3".to_owned()),
tag_input_string: Some("usenet, testing".to_owned()),
priority: Some(0),
..EditIndexerParams::default()
};
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"priority": 0,
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
],
"tags": [1, 2],
"id": 1
});
let (mock_details_server, app, mut server) = MockServarrApi::get()
.returns(indexer_details_json)
.path("/1")
.build_for(LidarrEvent::GetIndexers)
.await;
let mock_edit_server = server
.mock(
"PUT",
format!(
"/api/v1{}/1?forceSave=true",
LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource()
)
.as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
app.lock().await.data.lidarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params))
.await
);
mock_details_server.assert_async().await;
mock_edit_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_lidarr_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details()
{
let expected_edit_indexer_params = EditIndexerParams {
indexer_id: 1,
name: Some("Test Update".to_owned()),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: Some("https://localhost:9696/1/".to_owned()),
api_key: Some("test1234".to_owned()),
seed_ratio: Some("1.3".to_owned()),
tag_input_string: Some("usenet, testing".to_owned()),
priority: Some(0),
..EditIndexerParams::default()
};
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"priority": 0,
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.3",
},
],
"tags": [1, 2],
"id": 1
});
let (mock_details_server, app, mut server) = MockServarrApi::get()
.returns(indexer_details_json)
.path("/1")
.build_for(LidarrEvent::GetIndexers)
.await;
let mock_edit_server = server
.mock(
"PUT",
format!(
"/api/v1{}/1?forceSave=true",
LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource()
)
.as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
app.lock().await.data.lidarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params))
.await
);
mock_details_server.assert_async().await;
mock_edit_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_lidarr_indexer_event_defaults_to_previous_values() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let edit_indexer_params = EditIndexerParams {
indexer_id: 1,
..EditIndexerParams::default()
};
let (mock_details_server, app, mut server) = MockServarrApi::get()
.returns(indexer_details_json.clone())
.path("/1")
.build_for(LidarrEvent::GetIndexers)
.await;
let mock_edit_server = server
.mock(
"PUT",
format!(
"/api/v1{}/1?forceSave=true",
LidarrEvent::EditIndexer(edit_indexer_params.clone()).resource()
)
.as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(indexer_details_json))
.create_async()
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::EditIndexer(edit_indexer_params))
.await
);
mock_details_server.assert_async().await;
mock_edit_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_lidarr_indexer_event_clears_tags_when_clear_tags_is_true() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1, 2],
"id": 1
});
let expected_edit_indexer_body = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [],
"id": 1
});
let edit_indexer_params = EditIndexerParams {
indexer_id: 1,
clear_tags: true,
..EditIndexerParams::default()
};
let (async_details_server, app, mut server) = MockServarrApi::get()
.returns(indexer_details_json)
.path("/1")
.build_for(LidarrEvent::GetIndexers)
.await;
let async_edit_server = server
.mock(
"PUT",
format!(
"/api/v1{}/1?forceSave=true",
LidarrEvent::EditIndexer(edit_indexer_params.clone()).resource()
)
.as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_edit_indexer_body))
.create_async()
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::EditIndexer(edit_indexer_params))
.await
);
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_lidarr_indexers_event() {
let indexers_response_json = json!([{
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"supportsRss": true,
"supportsSearch": true,
"protocol": "torrent",
"priority": 25,
"downloadClientId": 0,
"name": "Test Indexer",
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"implementationName": "Torznab",
"implementation": "Torznab",
"configContract": "TorznabSettings",
"tags": [1],
"id": 1
}]);
let response: Vec<Indexer> = serde_json::from_value(indexers_response_json.clone()).unwrap();
let (async_server, app, _server) = MockServarrApi::get()
.returns(indexers_response_json)
.build_for(LidarrEvent::GetIndexers)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::Indexers(indexers) = network
.handle_lidarr_event(LidarrEvent::GetIndexers)
.await
.unwrap()
else {
panic!("Expected Indexers")
};
async_server.assert_async().await;
assert_eq!(
app.lock().await.data.lidarr_data.indexers.items,
vec![indexer()]
);
assert_eq!(indexers, response);
}
#[tokio::test]
async fn test_handle_test_lidarr_indexer_event_error() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let response_json = json!([
{
"isWarning": false,
"propertyName": "",
"errorMessage": "test failure",
"severity": "error"
}]);
let (async_details_server, app, mut server) = MockServarrApi::get()
.returns(indexer_details_json.clone())
.path("/1")
.build_for(LidarrEvent::GetIndexers)
.await;
let async_test_server = server
.mock(
"POST",
format!("/api/v1{}", LidarrEvent::TestIndexer(1).resource()).as_str(),
)
.with_status(400)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(indexer_details_json.clone()))
.with_body(response_json.to_string())
.create_async()
.await;
app
.lock()
.await
.data
.lidarr_data
.indexers
.set_items(vec![indexer()]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::Value(value) = network
.handle_lidarr_event(LidarrEvent::TestIndexer(1))
.await
.unwrap()
else {
panic!("Expected Value")
};
async_details_server.assert_async().await;
async_test_server.assert_async().await;
assert_eq!(
app.lock().await.data.lidarr_data.indexer_test_errors,
Some("\"test failure\"".to_owned())
);
assert_eq!(value, response_json);
}
#[tokio::test]
async fn test_handle_test_lidarr_indexer_event_success() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let (async_details_server, app, mut server) = MockServarrApi::get()
.returns(indexer_details_json.clone())
.path("/1")
.build_for(LidarrEvent::GetIndexers)
.await;
let async_test_server = server
.mock(
"POST",
format!("/api/v1{}", LidarrEvent::TestIndexer(1).resource()).as_str(),
)
.with_status(200)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(indexer_details_json.clone()))
.with_body("{}")
.create_async()
.await;
app
.lock()
.await
.data
.lidarr_data
.indexers
.set_items(vec![indexer()]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::Value(value) = network
.handle_lidarr_event(LidarrEvent::TestIndexer(1))
.await
.unwrap()
else {
panic!("Expected Value")
};
async_details_server.assert_async().await;
async_test_server.assert_async().await;
assert_eq!(
app.lock().await.data.lidarr_data.indexer_test_errors,
Some(String::new())
);
assert_eq!(value, json!({}));
}
#[tokio::test]
async fn test_handle_test_all_lidarr_indexers_event() {
let indexers = vec![
Indexer {
id: 1,
name: Some("Test 1".to_owned()),
..Indexer::default()
},
Indexer {
id: 2,
name: Some("Test 2".to_owned()),
..Indexer::default()
},
];
let indexer_test_results_modal_items = vec![
IndexerTestResultModalItem {
name: "Test 1".to_owned(),
is_valid: true,
validation_failures: HorizontallyScrollableText::default(),
},
IndexerTestResultModalItem {
name: "Test 2".to_owned(),
is_valid: false,
validation_failures: "Failure for field 'test field 1': test error message, Failure for field 'test field 2': test error message 2".into(),
},
];
let response_json = json!([
{
"id": 1,
"isValid": true,
"validationFailures": []
},
{
"id": 2,
"isValid": false,
"validationFailures": [
{
"propertyName": "test field 1",
"errorMessage": "test error message",
"severity": "error"
},
{
"propertyName": "test field 2",
"errorMessage": "test error message 2",
"severity": "error"
},
]
}]);
let response: Vec<IndexerTestResult> = serde_json::from_value(response_json.clone()).unwrap();
let (async_server, app, _server) = MockServarrApi::post()
.returns(response_json)
.status(400)
.build_for(LidarrEvent::TestAllIndexers)
.await;
app
.lock()
.await
.data
.lidarr_data
.indexers
.set_items(indexers);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::IndexerTestResults(results) = network
.handle_lidarr_event(LidarrEvent::TestAllIndexers)
.await
.unwrap()
else {
panic!("Expected IndexerTestResults")
};
async_server.assert_async().await;
assert_some!(&app.lock().await.data.lidarr_data.indexer_test_all_results);
assert_eq!(
app
.lock()
.await
.data
.lidarr_data
.indexer_test_all_results
.as_ref()
.unwrap()
.items,
indexer_test_results_modal_items
);
assert_eq!(results, response);
}
#[tokio::test]
async fn test_handle_test_all_lidarr_indexers_event_sets_empty_table_on_api_error() {
let (async_server, app, _server) = MockServarrApi::post()
.status(500)
.build_for(LidarrEvent::TestAllIndexers)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::TestAllIndexers)
.await;
async_server.assert_async().await;
assert_err!(result);
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.indexer_test_all_results);
assert_is_empty!(
app
.data
.lidarr_data
.indexer_test_all_results
.as_ref()
.unwrap()
.items
);
}
}
+419
View File
@@ -0,0 +1,419 @@
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_models::{
EditIndexerParams, Indexer, IndexerSettings, IndexerTestResult,
};
use crate::models::stateful_table::StatefulTable;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::{Context, Result};
use log::{debug, info};
use serde_json::{Value, json};
#[cfg(test)]
#[path = "lidarr_indexers_network_tests.rs"]
mod lidarr_indexers_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn delete_lidarr_indexer(
&mut self,
indexer_id: i64,
) -> Result<()> {
let event = LidarrEvent::DeleteIndexer(indexer_id);
info!("Deleting Lidarr indexer with id: {indexer_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{indexer_id}")),
None,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn edit_all_lidarr_indexer_settings(
&mut self,
params: IndexerSettings,
) -> Result<Value> {
info!("Updating Lidarr indexer settings");
let event = LidarrEvent::EditAllIndexerSettings(IndexerSettings::default());
debug!("Indexer settings body: {params:?}");
let request_props = self
.request_props_from(event, RequestMethod::Put, Some(params), None, None)
.await;
self
.handle_request::<IndexerSettings, Value>(request_props, |_, _| {})
.await
}
pub(in crate::network::lidarr_network) async fn get_all_lidarr_indexer_settings(
&mut self,
) -> Result<IndexerSettings> {
info!("Fetching Lidarr indexer settings");
let event = LidarrEvent::GetAllIndexerSettings;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), IndexerSettings>(request_props, |indexer_settings, mut app| {
if app.data.lidarr_data.indexer_settings.is_none() {
app.data.lidarr_data.indexer_settings = Some(indexer_settings);
} else {
debug!("Indexer Settings are being modified. Ignoring update...");
}
})
.await
}
pub(in crate::network::lidarr_network) async fn edit_lidarr_indexer(
&mut self,
mut edit_indexer_params: EditIndexerParams,
) -> Result<()> {
if let Some(tag_input_str) = edit_indexer_params.tag_input_string.as_ref() {
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
edit_indexer_params.tags = Some(tag_ids_vec);
}
let detail_event = LidarrEvent::GetIndexers;
let event = LidarrEvent::EditIndexer(EditIndexerParams::default());
let id = edit_indexer_params.indexer_id;
info!("Updating Lidarr indexer with ID: {id}");
info!("Fetching indexer details for indexer with ID: {id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_indexer_body, _| {
response = detailed_indexer_body.to_string()
})
.await?;
info!("Constructing edit indexer body");
let mut detailed_indexer_body: Value = serde_json::from_str(&response)?;
let (
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
tags,
priority,
) = {
let priority = detailed_indexer_body["priority"]
.as_i64()
.context("Failed to deserialize indexer 'priority' field")?;
let seed_ratio_field_option = detailed_indexer_body["fields"]
.as_array()
.context("Failed to get indexer 'fields' array")?
.iter()
.find(|field| field["name"] == "seedCriteria.seedRatio");
let name = edit_indexer_params.name.unwrap_or(
detailed_indexer_body["name"]
.as_str()
.context("Failed to deserialize indexer 'name' field")?
.to_owned(),
);
let enable_rss = edit_indexer_params.enable_rss.unwrap_or(
detailed_indexer_body["enableRss"]
.as_bool()
.context("Failed to deserialize indexer 'enableRss' field")?,
);
let enable_automatic_search = edit_indexer_params.enable_automatic_search.unwrap_or(
detailed_indexer_body["enableAutomaticSearch"]
.as_bool()
.context("Failed to deserialize indexer 'enableAutomaticSearch' field")?,
);
let enable_interactive_search = edit_indexer_params.enable_interactive_search.unwrap_or(
detailed_indexer_body["enableInteractiveSearch"]
.as_bool()
.context("Failed to deserialize indexer 'enableInteractiveSearch' field")?,
);
let url = edit_indexer_params.url.unwrap_or(
detailed_indexer_body["fields"]
.as_array()
.context("Failed to get indexer 'fields' array for baseUrl")?
.iter()
.find(|field| field["name"] == "baseUrl")
.context("Field 'baseUrl' was not found in the indexer fields array")?
.get("value")
.unwrap_or(&json!(""))
.as_str()
.context("Failed to deserialize indexer 'baseUrl' value")?
.to_owned(),
);
let api_key = edit_indexer_params.api_key.unwrap_or(
detailed_indexer_body["fields"]
.as_array()
.context("Failed to get indexer 'fields' array for apiKey")?
.iter()
.find(|field| field["name"] == "apiKey")
.context("Field 'apiKey' was not found in the indexer fields array")?
.get("value")
.unwrap_or(&json!(""))
.as_str()
.context("Failed to deserialize indexer 'apiKey' value")?
.to_owned(),
);
let seed_ratio = edit_indexer_params.seed_ratio.unwrap_or_else(|| {
if let Some(seed_ratio_field) = seed_ratio_field_option {
return seed_ratio_field
.get("value")
.unwrap_or(&json!(""))
.as_str()
.unwrap_or("")
.to_owned();
}
String::new()
});
let tags = if edit_indexer_params.clear_tags {
vec![]
} else {
edit_indexer_params.tags.unwrap_or(
detailed_indexer_body["tags"]
.as_array()
.context("Failed to get indexer 'tags' array")?
.iter()
.map(|item| {
item
.as_i64()
.context("Failed to deserialize indexer tag ID")
})
.collect::<Result<Vec<_>>>()?,
)
};
let priority = edit_indexer_params.priority.unwrap_or(priority);
(
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
tags,
priority,
)
};
*detailed_indexer_body
.get_mut("name")
.context("Failed to get mutable reference to indexer 'name' field")? = json!(name);
*detailed_indexer_body
.get_mut("priority")
.context("Failed to get mutable reference to indexer 'priority' field")? = json!(priority);
*detailed_indexer_body
.get_mut("enableRss")
.context("Failed to get mutable reference to indexer 'enableRss' field")? = json!(enable_rss);
*detailed_indexer_body
.get_mut("enableAutomaticSearch")
.context("Failed to get mutable reference to indexer 'enableAutomaticSearch' field")? =
json!(enable_automatic_search);
*detailed_indexer_body
.get_mut("enableInteractiveSearch")
.context("Failed to get mutable reference to indexer 'enableInteractiveSearch' field")? =
json!(enable_interactive_search);
*detailed_indexer_body
.get_mut("fields")
.and_then(|f| f.as_array_mut())
.context("Failed to get mutable reference to indexer 'fields' array")?
.iter_mut()
.find(|field| field["name"] == "baseUrl")
.context("Failed to find 'baseUrl' field in indexer fields array")?
.get_mut("value")
.context("Failed to get mutable reference to 'baseUrl' value")? = json!(url);
*detailed_indexer_body
.get_mut("fields")
.and_then(|f| f.as_array_mut())
.context("Failed to get mutable reference to indexer 'fields' array for apiKey")?
.iter_mut()
.find(|field| field["name"] == "apiKey")
.context("Failed to find 'apiKey' field in indexer fields array")?
.get_mut("value")
.context("Failed to get mutable reference to 'apiKey' value")? = json!(api_key);
*detailed_indexer_body
.get_mut("tags")
.context("Failed to get mutable reference to indexer 'tags' field")? = json!(tags);
let seed_ratio_field_option = detailed_indexer_body
.get_mut("fields")
.and_then(|f| f.as_array_mut())
.context("Failed to get mutable reference to indexer 'fields' array for seed ratio")?
.iter_mut()
.find(|field| field["name"] == "seedCriteria.seedRatio");
if let Some(seed_ratio_field) = seed_ratio_field_option {
seed_ratio_field
.as_object_mut()
.context("Failed to get mutable reference to 'seedCriteria.seedRatio' object")?
.insert("value".to_string(), json!(seed_ratio));
}
debug!("Edit indexer body: {detailed_indexer_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_indexer_body),
Some(format!("/{id}")),
Some("forceSave=true".to_owned()),
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_indexers(
&mut self,
) -> Result<Vec<Indexer>> {
info!("Fetching Lidarr indexers");
let event = LidarrEvent::GetIndexers;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Indexer>>(request_props, |indexers, mut app| {
app.data.lidarr_data.indexers.set_items(indexers);
})
.await
}
pub(in crate::network::lidarr_network) async fn test_lidarr_indexer(
&mut self,
indexer_id: i64,
) -> Result<Value> {
let detail_event = LidarrEvent::GetIndexers;
let event = LidarrEvent::TestIndexer(indexer_id);
info!("Testing Lidarr indexer with ID: {indexer_id}");
info!("Fetching indexer details for indexer with ID: {indexer_id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{indexer_id}")),
None,
)
.await;
let mut test_body: Value = Value::default();
self
.handle_request::<(), Value>(request_props, |detailed_indexer_body, _| {
test_body = detailed_indexer_body;
})
.await?;
info!("Testing indexer");
let mut request_props = self
.request_props_from(event, RequestMethod::Post, Some(test_body), None, None)
.await;
request_props.ignore_status_code = true;
self
.handle_request::<Value, Value>(request_props, |test_results, mut app| {
if test_results.as_object().is_none() {
let error_message = test_results
.as_array()
.and_then(|arr| arr.first())
.and_then(|item| item.get("errorMessage"))
.map(|msg| msg.to_string())
.unwrap_or_else(|| "Unknown indexer test error".to_string());
app.data.lidarr_data.indexer_test_errors = Some(error_message);
} else {
app.data.lidarr_data.indexer_test_errors = Some(String::new());
};
})
.await
}
pub(in crate::network::lidarr_network) async fn test_all_lidarr_indexers(
&mut self,
) -> Result<Vec<IndexerTestResult>> {
info!("Testing all Lidarr indexers");
let event = LidarrEvent::TestAllIndexers;
let mut request_props = self
.request_props_from(event, RequestMethod::Post, None, None, None)
.await;
request_props.ignore_status_code = true;
let result = self
.handle_request::<(), Vec<IndexerTestResult>>(request_props, |test_results, mut app| {
let mut test_all_indexer_results = StatefulTable::default();
let indexers = app.data.lidarr_data.indexers.items.clone();
let modal_test_results = test_results
.iter()
.map(|result| {
let name = indexers
.iter()
.filter(|&indexer| indexer.id == result.id)
.map(|indexer| indexer.name.clone())
.nth(0)
.unwrap_or_default();
let validation_failures = result
.validation_failures
.iter()
.map(|failure| {
format!(
"Failure for field '{}': {}",
failure.property_name, failure.error_message
)
})
.collect::<Vec<String>>()
.join(", ");
IndexerTestResultModalItem {
name: name.unwrap_or_default(),
is_valid: result.is_valid,
validation_failures: validation_failures.into(),
}
})
.collect();
test_all_indexer_results.set_items(modal_test_results);
app.data.lidarr_data.indexer_test_all_results = Some(test_all_indexer_results);
})
.await;
if result.is_err() {
self
.app
.lock()
.await
.data
.lidarr_data
.indexer_test_all_results = Some(StatefulTable::default());
}
result
}
}
@@ -2,18 +2,20 @@
mod tests {
use crate::models::lidarr_models::{
AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams,
LidarrSerdeable, MonitorType, NewItemMonitorType,
LidarrHistoryItem, LidarrSerdeable, MonitorType, NewItemMonitorType,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::{SortOption, StatefulTable};
use crate::network::NetworkResource;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON,
ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON, artist, lidarr_history_item,
};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use bimap::BiMap;
use mockito::Matcher;
use pretty_assertions::assert_eq;
use rstest::rstest;
use serde_json::{Value, json};
#[tokio::test]
@@ -101,6 +103,296 @@ mod tests {
assert_eq!(artist, expected_artist);
}
#[rstest]
#[tokio::test]
async fn test_handle_get_lidarr_artist_history_event(
#[values(true, false)] use_custom_sorting: bool,
) {
let history_json = json!([{
"id": 123,
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]);
let response: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let mut expected_history_items = vec![
LidarrHistoryItem {
id: 123,
artist_id: 1007,
album_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
];
let (async_server, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=1")
.build_for(LidarrEvent::GetArtistHistory(1))
.await;
let mut artist_history_table = StatefulTable {
sort_asc: true,
..StatefulTable::default()
};
if use_custom_sorting {
let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
expected_history_items.sort_by(cmp_fn);
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
artist_history_table.sorting(vec![history_sort_option]);
}
app
.lock()
.await
.data
.lidarr_data
.artists
.set_items(vec![artist()]);
app.lock().await.data.lidarr_data.artist_history = Some(artist_history_table);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history_items) = network
.handle_lidarr_event(LidarrEvent::GetArtistHistory(1))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
async_server.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.artist_history);
assert_eq!(
app.data.lidarr_data.artist_history.as_ref().unwrap().items,
expected_history_items
);
assert!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.sort_asc
);
assert_eq!(history_items, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_artist_history_event_empty_artist_history_table() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]);
let response: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let expected_history_items = vec![
LidarrHistoryItem {
id: 123,
artist_id: 1007,
album_id: 1007,
source_title: "z album".into(),
..lidarr_history_item()
},
LidarrHistoryItem {
id: 456,
artist_id: 2001,
album_id: 2001,
source_title: "An Album".into(),
..lidarr_history_item()
},
];
let (async_server, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=1")
.build_for(LidarrEvent::GetArtistHistory(1))
.await;
app
.lock()
.await
.data
.lidarr_data
.artists
.set_items(vec![artist()]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history_items) = network
.handle_lidarr_event(LidarrEvent::GetArtistHistory(1))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
async_server.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.artist_history);
assert_eq!(
app.data.lidarr_data.artist_history.as_ref().unwrap().items,
expected_history_items
);
assert!(
!app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.sort_asc
);
assert_eq!(history_items, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_artist_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z album",
"albumId": 1007,
"artistId": 1007,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
},
{
"id": 456,
"sourceTitle": "An Album",
"albumId": 2001,
"artistId": 2001,
"quality": { "quality": { "name": "Lossless" } },
"date": "2023-01-01T00:00:00Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3",
"importedPath": "/nfs/music/Something/Album 1/Cool.mp3"
}
}]);
let response: Vec<LidarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let (async_server, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("artistId=1")
.build_for(LidarrEvent::GetArtistHistory(1))
.await;
let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
let mut artist_history_table = StatefulTable {
sort_asc: true,
..StatefulTable::default()
};
artist_history_table.sorting(vec![history_sort_option]);
app.lock().await.data.lidarr_data.artist_history = Some(artist_history_table);
app
.lock()
.await
.data
.lidarr_data
.artists
.set_items(vec![artist()]);
app
.lock()
.await
.push_navigation_stack(ActiveLidarrBlock::ArtistHistorySortPrompt.into());
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LidarrHistoryItems(history_items) = network
.handle_lidarr_event(LidarrEvent::GetArtistHistory(1))
.await
.unwrap()
else {
panic!("Expected LidarrHistoryItems")
};
async_server.assert_async().await;
let app = app.lock().await;
assert_some!(&app.data.lidarr_data.artist_history);
assert!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.is_empty()
);
assert!(
app
.data
.lidarr_data
.artist_history
.as_ref()
.unwrap()
.sort_asc
);
assert_eq!(history_items, response);
}
#[tokio::test]
async fn test_handle_toggle_artist_monitoring_event() {
let artist_json = json!({
@@ -5,6 +5,7 @@ use serde_json::{Value, json};
use crate::models::Route;
use crate::models::lidarr_models::{
AddArtistBody, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, LidarrCommandBody,
LidarrHistoryItem,
};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::stateful_table::StatefulTable;
@@ -281,6 +282,41 @@ impl Network<'_, '_> {
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_artist_history(
&mut self,
artist_id: i64,
) -> Result<Vec<LidarrHistoryItem>> {
info!("Fetching Lidarr artist history for artist with ID: {artist_id}");
let event = LidarrEvent::GetArtistHistory(artist_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("artistId={artist_id}")),
)
.await;
self
.handle_request::<(), Vec<LidarrHistoryItem>>(request_props, |mut history_vec, mut app| {
let is_sorting = matches!(
app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::ArtistHistorySortPrompt, _)
);
let artist_history = app.data.lidarr_data.artist_history.get_or_insert_default();
if !is_sorting {
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
artist_history.set_items(history_vec);
artist_history.apply_sorting_toggle(false);
}
})
.await
}
pub(in crate::network::lidarr_network) async fn edit_artist(
&mut self,
mut edit_artist_params: EditArtistParams,
@@ -1,17 +1,21 @@
#[cfg(test)]
#[allow(dead_code)]
pub mod test_utils {
use crate::models::HorizontallyScrollableText;
use crate::models::lidarr_models::{
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
DownloadRecord, DownloadStatus, DownloadsResponse, EditArtistParams, LidarrHistoryData,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, Member, MetadataProfile,
NewItemMonitorType, Ratings, SystemStatus,
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrTask, LidarrTaskName,
Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus,
};
use crate::models::servarr_models::{Quality, QualityProfile, QualityWrapper, RootFolder, Tag};
use crate::models::servarr_models::IndexerSettings;
use crate::models::servarr_models::{
Indexer, IndexerField, Quality, QualityProfile, QualityWrapper, RootFolder, Tag,
};
use crate::models::{HorizontallyScrollableText, ScrollableText};
use bimap::BiMap;
use chrono::DateTime;
use serde_json::Number;
use indoc::formatdoc;
use serde_json::{Number, json};
pub const ADD_ARTIST_SEARCH_RESULT_JSON: &str = r#"{
"foreignArtistId": "test-foreign-id",
@@ -287,4 +291,90 @@ pub mod test_utils {
..LidarrHistoryData::default()
}
}
pub fn indexer() -> Indexer {
Indexer {
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
supports_rss: true,
supports_search: true,
protocol: "torrent".to_owned(),
priority: 25,
download_client_id: 0,
name: Some("Test Indexer".to_owned()),
implementation_name: Some("Torznab".to_owned()),
implementation: Some("Torznab".to_owned()),
config_contract: Some("TorznabSettings".to_owned()),
tags: vec![Number::from(1)],
id: 1,
fields: Some(vec![
IndexerField {
name: Some("baseUrl".to_owned()),
value: Some(json!("https://test.com")),
},
IndexerField {
name: Some("apiKey".to_owned()),
value: Some(json!("")),
},
IndexerField {
name: Some("seedCriteria.seedRatio".to_owned()),
value: Some(json!("1.2")),
},
]),
}
}
pub fn indexer_settings() -> IndexerSettings {
IndexerSettings {
id: 1,
minimum_age: 1,
retention: 1,
maximum_size: 12345,
rss_sync_interval: 60,
}
}
pub fn task() -> LidarrTask {
LidarrTask {
name: "Backup".to_owned(),
task_name: LidarrTaskName::Backup,
interval: 60,
last_execution: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()),
next_execution: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T22:29:16Z").unwrap()),
}
}
pub fn log_line() -> &'static str {
"2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process"
}
pub fn updates() -> ScrollableText {
let line_break = "-".repeat(200);
ScrollableText::with_string(formatdoc!(
"
The latest version of Lidarr is already installed
4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed)
{line_break}
New:
* Cool new thing
Fixed:
* Some bugs killed
3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed)
{line_break}
New:
* Cool new thing (old)
* Other cool new thing (old)
2.1.0 - 2023-04-15 02:02:53 UTC
{line_break}
Fixed:
* Killed bug 1
* Fixed bug 2"
))
}
}
@@ -5,7 +5,7 @@ mod tests {
AddArtistBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
};
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings, QualityProfile, Tag};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent};
use bimap::BiMap;
@@ -15,6 +15,17 @@ mod tests {
use std::sync::Arc;
use tokio::sync::Mutex;
#[rstest]
fn test_resource_all_indexer_settings(
#[values(
LidarrEvent::GetAllIndexerSettings,
LidarrEvent::EditAllIndexerSettings(IndexerSettings::default())
)]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/config/indexer");
}
#[rstest]
fn test_resource_artist(
#[values(
@@ -37,6 +48,18 @@ mod tests {
assert_str_eq!(event.resource(), "/queue");
}
#[rstest]
fn test_resource_indexer(
#[values(
LidarrEvent::GetIndexers,
LidarrEvent::DeleteIndexer(0),
LidarrEvent::EditIndexer(EditIndexerParams::default())
)]
event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/indexer");
}
#[rstest]
fn test_resource_history(#[values(LidarrEvent::GetHistory(0))] event: LidarrEvent) {
assert_str_eq!(event.resource(), "/history");
@@ -67,7 +90,9 @@ mod tests {
LidarrEvent::UpdateAllArtists,
LidarrEvent::TriggerAutomaticArtistSearch(0),
LidarrEvent::UpdateAndScanArtist(0),
LidarrEvent::UpdateDownloads
LidarrEvent::UpdateDownloads,
LidarrEvent::GetQueuedEvents,
LidarrEvent::StartTask(Default::default())
)]
event: LidarrEvent,
) {
@@ -105,8 +130,14 @@ mod tests {
#[case(LidarrEvent::GetQualityProfiles, "/qualityprofile")]
#[case(LidarrEvent::GetStatus, "/system/status")]
#[case(LidarrEvent::GetTags, "/tag")]
#[case(LidarrEvent::GetLogs(500), "/log")]
#[case(LidarrEvent::GetTasks, "/system/task")]
#[case(LidarrEvent::GetUpdates, "/update")]
#[case(LidarrEvent::HealthCheck, "/health")]
#[case(LidarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")]
#[case(LidarrEvent::GetArtistHistory(0), "/history/artist")]
#[case(LidarrEvent::TestIndexer(0), "/indexer/test")]
#[case(LidarrEvent::TestAllIndexers, "/indexer/testall")]
fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) {
assert_str_eq!(event.resource(), expected_uri);
}
+74 -3
View File
@@ -4,13 +4,14 @@ use log::info;
use super::{NetworkEvent, NetworkResource};
use crate::models::lidarr_models::{
AddArtistBody, AddLidarrRootFolderBody, DeleteParams, EditArtistParams, LidarrSerdeable,
MetadataProfile,
LidarrTaskName, MetadataProfile,
};
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings, QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
mod downloads;
mod history;
mod indexers;
mod library;
mod root_folders;
mod system;
@@ -31,26 +32,39 @@ pub enum LidarrEvent {
DeleteAlbum(DeleteParams),
DeleteArtist(DeleteParams),
DeleteDownload(i64),
DeleteIndexer(i64),
DeleteRootFolder(i64),
DeleteTag(i64),
EditArtist(EditArtistParams),
EditAllIndexerSettings(IndexerSettings),
EditIndexer(EditIndexerParams),
GetAlbums(i64),
GetAlbumDetails(i64),
GetArtistHistory(i64),
GetAllIndexerSettings,
GetArtistDetails(i64),
GetDiskSpace,
GetDownloads(u64),
GetHistory(u64),
GetHostConfig,
GetIndexers,
GetLogs(u64),
MarkHistoryItemAsFailed(i64),
GetMetadataProfiles,
GetQualityProfiles,
GetQueuedEvents,
GetRootFolders,
GetSecurityConfig,
GetStatus,
GetUpdates,
GetTags,
GetTasks,
HealthCheck,
ListArtists,
SearchNewArtist(String),
StartTask(LidarrTaskName),
TestIndexer(i64),
TestAllIndexers,
ToggleAlbumMonitoring(i64),
ToggleArtistMonitoring(i64),
TriggerAutomaticArtistSearch(i64),
@@ -63,6 +77,9 @@ impl NetworkResource for LidarrEvent {
fn resource(&self) -> &'static str {
match &self {
LidarrEvent::AddTag(_) | LidarrEvent::DeleteTag(_) | LidarrEvent::GetTags => "/tag",
LidarrEvent::GetAllIndexerSettings | LidarrEvent::EditAllIndexerSettings(_) => {
"/config/indexer"
}
LidarrEvent::DeleteArtist(_)
| LidarrEvent::EditArtist(_)
| LidarrEvent::GetArtistDetails(_)
@@ -73,21 +90,32 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::ToggleAlbumMonitoring(_)
| LidarrEvent::GetAlbumDetails(_)
| LidarrEvent::DeleteAlbum(_) => "/album",
LidarrEvent::GetArtistHistory(_) => "/history/artist",
LidarrEvent::GetLogs(_) => "/log",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue",
LidarrEvent::GetHistory(_) => "/history",
LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::GetIndexers | LidarrEvent::DeleteIndexer(_) | LidarrEvent::EditIndexer(_) => {
"/indexer"
}
LidarrEvent::TriggerAutomaticArtistSearch(_)
| LidarrEvent::UpdateAllArtists
| LidarrEvent::UpdateAndScanArtist(_)
| LidarrEvent::UpdateDownloads => "/command",
| LidarrEvent::UpdateDownloads
| LidarrEvent::GetQueuedEvents
| LidarrEvent::StartTask(_) => "/command",
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
LidarrEvent::GetQualityProfiles => "/qualityprofile",
LidarrEvent::GetRootFolders
| LidarrEvent::AddRootFolder(_)
| LidarrEvent::DeleteRootFolder(_) => "/rootfolder",
LidarrEvent::TestIndexer(_) => "/indexer/test",
LidarrEvent::TestAllIndexers => "/indexer/testall",
LidarrEvent::GetStatus => "/system/status",
LidarrEvent::GetTasks => "/system/task",
LidarrEvent::GetUpdates => "/update",
LidarrEvent::HealthCheck => "/health",
LidarrEvent::SearchNewArtist(_) => "/artist/lookup",
}
@@ -121,6 +149,18 @@ impl Network<'_, '_> {
.delete_lidarr_download(download_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::EditAllIndexerSettings(params) => self
.edit_all_lidarr_indexer_settings(params)
.await
.map(LidarrSerdeable::from),
LidarrEvent::EditIndexer(params) => self
.edit_lidarr_indexer(params)
.await
.map(LidarrSerdeable::from),
LidarrEvent::DeleteIndexer(indexer_id) => self
.delete_lidarr_indexer(indexer_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::DeleteRootFolder(root_folder_id) => self
.delete_lidarr_root_folder(root_folder_id)
.await
@@ -132,6 +172,10 @@ impl Network<'_, '_> {
LidarrEvent::GetAlbums(artist_id) => {
self.get_albums(artist_id).await.map(LidarrSerdeable::from)
}
LidarrEvent::GetAllIndexerSettings => self
.get_all_lidarr_indexer_settings()
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetArtistDetails(artist_id) => self
.get_artist_details(artist_id)
.await
@@ -145,10 +189,19 @@ impl Network<'_, '_> {
.get_lidarr_downloads(count)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetIndexers => self.get_lidarr_indexers().await.map(LidarrSerdeable::from),
LidarrEvent::GetHistory(events) => self
.get_lidarr_history(events)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetArtistHistory(artist_id) => self
.get_lidarr_artist_history(artist_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetLogs(events) => self
.get_lidarr_logs(events)
.await
.map(LidarrSerdeable::from),
LidarrEvent::MarkHistoryItemAsFailed(history_item_id) => self
.mark_lidarr_history_item_as_failed(history_item_id)
.await
@@ -165,6 +218,10 @@ impl Network<'_, '_> {
.get_lidarr_quality_profiles()
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetQueuedEvents => self
.get_queued_lidarr_events()
.await
.map(LidarrSerdeable::from),
LidarrEvent::GetRootFolders => self
.get_lidarr_root_folders()
.await
@@ -175,6 +232,8 @@ impl Network<'_, '_> {
.map(LidarrSerdeable::from),
LidarrEvent::GetStatus => self.get_lidarr_status().await.map(LidarrSerdeable::from),
LidarrEvent::GetTags => self.get_lidarr_tags().await.map(LidarrSerdeable::from),
LidarrEvent::GetTasks => self.get_lidarr_tasks().await.map(LidarrSerdeable::from),
LidarrEvent::GetUpdates => self.get_lidarr_updates().await.map(LidarrSerdeable::from),
LidarrEvent::HealthCheck => self
.get_lidarr_healthcheck()
.await
@@ -183,6 +242,10 @@ impl Network<'_, '_> {
LidarrEvent::SearchNewArtist(query) => {
self.search_artist(query).await.map(LidarrSerdeable::from)
}
LidarrEvent::StartTask(task_name) => self
.start_lidarr_task(task_name)
.await
.map(LidarrSerdeable::from),
LidarrEvent::ToggleAlbumMonitoring(album_id) => self
.toggle_album_monitoring(album_id)
.await
@@ -206,6 +269,14 @@ impl Network<'_, '_> {
.update_lidarr_downloads()
.await
.map(LidarrSerdeable::from),
LidarrEvent::TestAllIndexers => self
.test_all_lidarr_indexers()
.await
.map(LidarrSerdeable::from),
LidarrEvent::TestIndexer(indexer_id) => self
.test_lidarr_indexer(indexer_id)
.await
.map(LidarrSerdeable::from),
}
}
@@ -1,9 +1,14 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::{LidarrSerdeable, SystemStatus};
use crate::models::servarr_models::{DiskSpace, HostConfig, SecurityConfig};
use crate::models::HorizontallyScrollableText;
use crate::models::lidarr_models::{LidarrSerdeable, LidarrTask, LidarrTaskName, SystemStatus};
use crate::models::servarr_models::{
DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update,
};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::updates;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use chrono::DateTime;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -104,6 +109,117 @@ mod tests {
assert_eq!(security_config, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_logs_event() {
let expected_logs = vec![
HorizontallyScrollableText::from(
"2023-05-20 21:29:16 UTC|FATAL|LidarrError|Some.Big.Bad.Exception|test exception",
),
HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"),
];
let logs_response_json = json!({
"page": 1,
"pageSize": 500,
"sortKey": "time",
"sortDirection": "descending",
"totalRecords": 2,
"records": [
{
"time": "2023-05-20T21:29:16Z",
"level": "info",
"logger": "TestLogger",
"message": "test message",
"id": 1
},
{
"time": "2023-05-20T21:29:16Z",
"level": "fatal",
"logger": "LidarrError",
"exception": "test exception",
"exceptionType": "Some.Big.Bad.Exception",
"id": 2
}
]
});
let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(logs_response_json)
.query("pageSize=500&sortDirection=descending&sortKey=time")
.build_for(LidarrEvent::GetLogs(500))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::LogResponse(logs) = network
.handle_lidarr_event(LidarrEvent::GetLogs(500))
.await
.unwrap()
else {
panic!("Expected LogResponse")
};
mock.assert_async().await;
assert_eq!(app.lock().await.data.lidarr_data.logs.items, expected_logs);
assert!(
app
.lock()
.await
.data
.lidarr_data
.logs
.current_selection()
.text
.contains("INFO")
);
assert_eq!(logs, response);
}
#[tokio::test]
async fn test_handle_get_queued_lidarr_events_event() {
let queued_events_json = json!([{
"name": "RefreshMonitoredDownloads",
"commandName": "Refresh Monitored Downloads",
"status": "completed",
"queued": "2023-05-20T21:29:16Z",
"started": "2023-05-20T21:29:16Z",
"ended": "2023-05-20T21:29:16Z",
"duration": "00:00:00.5111547",
"trigger": "scheduled",
}]);
let response: Vec<QueueEvent> = serde_json::from_value(queued_events_json.clone()).unwrap();
let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap());
let expected_event = QueueEvent {
name: "RefreshMonitoredDownloads".to_owned(),
command_name: "Refresh Monitored Downloads".to_owned(),
status: "completed".to_owned(),
queued: timestamp,
started: Some(timestamp),
ended: Some(timestamp),
duration: Some("00:00:00.5111547".to_owned()),
trigger: "scheduled".to_owned(),
};
let (mock, app, _server) = MockServarrApi::get()
.returns(queued_events_json)
.build_for(LidarrEvent::GetQueuedEvents)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::QueueEvents(events) = network
.handle_lidarr_event(LidarrEvent::GetQueuedEvents)
.await
.unwrap()
else {
panic!("Expected QueueEvents")
};
mock.assert_async().await;
assert_eq!(
app.lock().await.data.lidarr_data.queued_events.items,
vec![expected_event]
);
assert_eq!(events, response);
}
#[tokio::test]
async fn test_handle_get_status_event() {
let status_json = json!({
@@ -129,4 +245,171 @@ mod tests {
assert_eq!(status, response);
assert_eq!(app.lock().await.data.lidarr_data.version, "1.0.0");
}
#[tokio::test]
async fn test_handle_get_lidarr_tasks_event() {
let tasks_json = json!([{
"name": "Application Update Check",
"taskName": "ApplicationUpdateCheck",
"interval": 360,
"lastExecution": "2023-05-20T21:29:16Z",
"nextExecution": "2023-05-20T21:29:16Z",
},
{
"name": "Backup",
"taskName": "Backup",
"interval": 10080,
"lastExecution": "2023-05-20T21:29:16Z",
"nextExecution": "2023-05-20T21:29:16Z",
}]);
let response: Vec<LidarrTask> = serde_json::from_value(tasks_json.clone()).unwrap();
let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap());
let expected_tasks = vec![
LidarrTask {
name: "Application Update Check".to_owned(),
task_name: LidarrTaskName::ApplicationUpdateCheck,
interval: 360,
last_execution: timestamp,
next_execution: timestamp,
},
LidarrTask {
name: "Backup".to_owned(),
task_name: LidarrTaskName::Backup,
interval: 10080,
last_execution: timestamp,
next_execution: timestamp,
},
];
let (mock, app, _server) = MockServarrApi::get()
.returns(tasks_json)
.build_for(LidarrEvent::GetTasks)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::Tasks(tasks) = network
.handle_lidarr_event(LidarrEvent::GetTasks)
.await
.unwrap()
else {
panic!("Expected Tasks")
};
mock.assert_async().await;
assert_eq!(
app.lock().await.data.lidarr_data.tasks.items,
expected_tasks
);
assert_eq!(tasks, response);
}
#[tokio::test]
async fn test_handle_get_lidarr_updates_event() {
let updates_json = json!([{
"version": "4.3.2.1",
"releaseDate": "2023-04-15T02:02:53Z",
"installed": true,
"installedOn": "2023-04-15T02:02:53Z",
"latest": true,
"changes": {
"new": [
"Cool new thing"
],
"fixed": [
"Some bugs killed"
]
},
},
{
"version": "3.2.1.0",
"releaseDate": "2023-04-15T02:02:53Z",
"installed": false,
"installedOn": "2023-04-15T02:02:53Z",
"latest": false,
"changes": {
"new": [
"Cool new thing (old)",
"Other cool new thing (old)"
],
},
},
{
"version": "2.1.0",
"releaseDate": "2023-04-15T02:02:53Z",
"installed": false,
"latest": false,
"changes": {
"fixed": [
"Killed bug 1",
"Fixed bug 2"
]
},
}]);
let response: Vec<Update> = serde_json::from_value(updates_json.clone()).unwrap();
let expected_text = updates();
let (mock, app, _server) = MockServarrApi::get()
.returns(updates_json)
.build_for(LidarrEvent::GetUpdates)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::Updates(updates) = network
.handle_lidarr_event(LidarrEvent::GetUpdates)
.await
.unwrap()
else {
panic!("Expected Updates")
};
mock.assert_async().await;
let actual_text = app.lock().await.data.lidarr_data.updates.get_text();
let expected = expected_text.get_text();
// Trim trailing whitespace from each line for comparison
let actual_trimmed: Vec<&str> = actual_text.lines().map(|l| l.trim_end()).collect();
let expected_trimmed: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect();
assert_eq!(
actual_trimmed, expected_trimmed,
"Updates text mismatch (after trimming trailing whitespace)"
);
assert_eq!(updates, response);
}
#[tokio::test]
async fn test_handle_start_lidarr_task_event() {
let response = json!({ "test": "test"});
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "ApplicationUpdateCheck"
}))
.returns(response.clone())
.build_for(LidarrEvent::StartTask(
LidarrTaskName::ApplicationUpdateCheck,
))
.await;
app
.lock()
.await
.data
.lidarr_data
.tasks
.set_items(vec![LidarrTask {
task_name: LidarrTaskName::default(),
..LidarrTask::default()
}]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let LidarrSerdeable::Value(value) = network
.handle_lidarr_event(LidarrEvent::StartTask(
LidarrTaskName::ApplicationUpdateCheck,
))
.await
.unwrap()
else {
panic!("Expected Value")
};
mock.assert_async().await;
assert_eq!(value, response);
}
}
+211 -11
View File
@@ -1,10 +1,14 @@
use anyhow::Result;
use log::info;
use crate::models::lidarr_models::SystemStatus;
use crate::models::servarr_models::{DiskSpace, HostConfig, SecurityConfig};
use crate::models::lidarr_models::{LidarrTask, LidarrTaskName, SystemStatus};
use crate::models::servarr_models::{
CommandBody, DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update,
};
use crate::models::{HorizontallyScrollableText, Scrollable, ScrollableText};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use indoc::formatdoc;
use log::info;
use serde_json::Value;
#[cfg(test)]
#[path = "lidarr_system_network_tests.rs"]
@@ -26,18 +30,62 @@ impl Network<'_, '_> {
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_security_config(
pub(in crate::network::lidarr_network) async fn get_lidarr_logs(
&mut self,
) -> Result<SecurityConfig> {
info!("Fetching Lidarr security config");
let event = LidarrEvent::GetSecurityConfig;
events: u64,
) -> Result<LogResponse> {
info!("Fetching Lidarr logs");
let event = LidarrEvent::GetLogs(events);
let params = format!("pageSize={events}&sortDirection=descending&sortKey=time");
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
.await;
self
.handle_request::<(), SecurityConfig>(request_props, |_, _| ())
.handle_request::<(), LogResponse>(request_props, |log_response, mut app| {
let mut logs = log_response.records;
logs.reverse();
let log_lines = logs
.into_iter()
.map(|log| {
if log.exception.is_some() {
HorizontallyScrollableText::from(format!(
"{}|{}|{}|{}|{}",
log.time,
log.level.to_uppercase(),
log
.logger
.as_ref()
.expect("logger must exist when exception is present"),
log
.exception_type
.as_ref()
.expect("exception_type must exist when exception is present"),
log
.exception
.as_ref()
.expect("exception must exist in this branch")
))
} else {
HorizontallyScrollableText::from(format!(
"{}|{}|{}|{}",
log.time,
log.level.to_uppercase(),
log.logger.as_ref().expect("logger must exist in log entry"),
log
.message
.as_ref()
.expect("message must exist when exception is not present")
))
}
})
.collect();
app.data.lidarr_data.logs.set_items(log_lines);
app.data.lidarr_data.logs.scroll_to_bottom();
})
.await
}
@@ -58,6 +106,42 @@ impl Network<'_, '_> {
.await
}
pub(in crate::network::lidarr_network) async fn get_queued_lidarr_events(
&mut self,
) -> Result<Vec<QueueEvent>> {
info!("Fetching Lidarr queued events");
let event = LidarrEvent::GetQueuedEvents;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<QueueEvent>>(request_props, |queued_events_vec, mut app| {
app
.data
.lidarr_data
.queued_events
.set_items(queued_events_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_security_config(
&mut self,
) -> Result<SecurityConfig> {
info!("Fetching Lidarr security config");
let event = LidarrEvent::GetSecurityConfig;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), SecurityConfig>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_status(
&mut self,
) -> Result<SystemStatus> {
@@ -75,4 +159,120 @@ impl Network<'_, '_> {
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_tasks(
&mut self,
) -> Result<Vec<LidarrTask>> {
info!("Fetching Lidarr tasks");
let event = LidarrEvent::GetTasks;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<LidarrTask>>(request_props, |tasks_vec, mut app| {
app.data.lidarr_data.tasks.set_items(tasks_vec);
})
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_updates(
&mut self,
) -> Result<Vec<Update>> {
info!("Fetching Lidarr updates");
let event = LidarrEvent::GetUpdates;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Update>>(request_props, |updates_vec, mut app| {
let latest_installed = if updates_vec
.iter()
.any(|update| update.latest && update.installed_on.is_some())
{
"already".to_owned()
} else {
"not".to_owned()
};
let updates = updates_vec
.into_iter()
.map(|update| {
let install_status = if update.installed_on.is_some() {
if update.installed {
" (Currently Installed)".to_owned()
} else {
" (Previously Installed)".to_owned()
}
} else {
String::new()
};
let vec_to_bullet_points = |vec: Vec<String>| {
vec
.iter()
.map(|change| format!(" * {change}"))
.collect::<Vec<String>>()
.join("\n")
};
let mut update_info = formatdoc!(
"{} - {}{install_status}
{}",
update.version,
update.release_date,
"-".repeat(200)
);
if let Some(new_changes) = update.changes.new {
let changes = vec_to_bullet_points(new_changes);
update_info = formatdoc!(
"{update_info}
New:
{changes}"
)
}
if let Some(fixes) = update.changes.fixed {
let fixes = vec_to_bullet_points(fixes);
update_info = formatdoc!(
"{update_info}
Fixed:
{fixes}"
);
}
update_info
})
.reduce(|version_1, version_2| format!("{version_1}\n\n\n{version_2}"))
.unwrap();
app.data.lidarr_data.updates = ScrollableText::with_string(formatdoc!(
"The latest version of Lidarr is {latest_installed} installed
{updates}"
));
})
.await
}
pub(in crate::network::lidarr_network) async fn start_lidarr_task(
&mut self,
task: LidarrTaskName,
) -> Result<Value> {
let event = LidarrEvent::StartTask(task);
let task_name = task.to_string();
info!("Starting Lidarr task: {task_name}");
let body = CommandBody { name: task_name };
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
}
+11 -1
View File
@@ -1,5 +1,5 @@
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_models::{DiskSpace, QueueEvent};
use crate::models::servarr_models::{DiskSpace, IndexerSettings, QueueEvent};
use chrono::DateTime;
pub fn diskspace() -> DiskSpace {
@@ -9,6 +9,16 @@ pub fn diskspace() -> DiskSpace {
}
}
pub fn indexer_settings() -> IndexerSettings {
IndexerSettings {
id: 1,
minimum_age: 1,
retention: 1,
maximum_size: 12345,
rss_sync_interval: 60,
}
}
pub fn indexer_test_result() -> IndexerTestResultModalItem {
IndexerTestResultModalItem {
name: "DrunkenSlug".to_owned(),
@@ -5,7 +5,7 @@ mod tests {
use crate::models::stateful_table::SortOption;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::sonarr_network::SonarrEvent;
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::history_item;
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::sonarr_history_item;
use pretty_assertions::assert_eq;
use rstest::rstest;
use serde_json::json;
@@ -45,13 +45,13 @@ mod tests {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
..sonarr_history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
..sonarr_history_item()
},
];
let (mock, app, _server) = MockServarrApi::get()
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_models::IndexerSettings;
use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult};
use crate::models::sonarr_models::IndexerSettings;
use crate::models::stateful_table::StatefulTable;
use crate::network::sonarr_network::SonarrEvent;
use crate::network::{Network, RequestMethod};
@@ -6,10 +6,9 @@ mod tests {
use crate::models::sonarr_models::SonarrSerdeable;
use crate::network::NetworkResource;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::servarr_test_utils::indexer_settings;
use crate::network::sonarr_network::SonarrEvent;
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
indexer, indexer_settings,
};
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::indexer;
use bimap::BiMap;
use mockito::Matcher;
use pretty_assertions::assert_eq;
@@ -31,11 +30,10 @@ mod tests {
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_sonarr_event(SonarrEvent::DeleteIndexer(1))
.await
.is_ok()
);
mock.assert_async().await;
@@ -58,11 +56,10 @@ mod tests {
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_sonarr_event(SonarrEvent::EditAllIndexerSettings(indexer_settings()))
.await
.is_ok()
);
mock.assert_async().await;
@@ -153,11 +150,10 @@ mod tests {
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params))
.await
.is_ok()
);
mock_details_server.assert_async().await;
@@ -248,11 +244,10 @@ mod tests {
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params))
.await
.is_ok()
);
mock_details_server.assert_async().await;
@@ -338,11 +333,10 @@ mod tests {
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params))
.await
.is_ok()
);
mock_details_server.assert_async().await;
@@ -435,11 +429,10 @@ mod tests {
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params))
.await
.is_ok()
);
mock_details_server.assert_async().await;
@@ -497,11 +490,10 @@ mod tests {
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params))
.await
.is_ok()
);
mock_details_server.assert_async().await;
@@ -584,11 +576,10 @@ mod tests {
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
assert!(
assert_ok!(
network
.handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params))
.await
.is_ok()
);
async_details_server.assert_async().await;
@@ -642,6 +633,7 @@ mod tests {
else {
panic!("Expected Indexers")
};
async_server.assert_async().await;
assert_eq!(
app.lock().await.data.sonarr_data.indexers.items,
@@ -714,6 +706,7 @@ mod tests {
else {
panic!("Expected Value")
};
async_details_server.assert_async().await;
async_test_server.assert_async().await;
assert_eq!(
@@ -780,6 +773,7 @@ mod tests {
else {
panic!("Expected Value")
};
async_details_server.assert_async().await;
async_test_server.assert_async().await;
assert_eq!(
@@ -860,16 +854,9 @@ mod tests {
else {
panic!("Expected IndexerTestResults")
};
async_server.assert_async().await;
assert!(
app
.lock()
.await
.data
.sonarr_data
.indexer_test_all_results
.is_some()
);
assert_some!(&app.lock().await.data.sonarr_data.indexer_test_all_results);
assert_eq!(
app
.lock()
@@ -12,7 +12,7 @@ mod tests {
use crate::network::sonarr_network::SonarrEvent;
use crate::network::sonarr_network::library::episodes::get_episode_status;
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
EPISODE_JSON, episode, episode_file, history_item, torrent_release,
EPISODE_JSON, episode, episode_file, sonarr_history_item, torrent_release,
};
use indoc::formatdoc;
use mockito::Matcher;
@@ -522,13 +522,13 @@ mod tests {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
..sonarr_history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
..sonarr_history_item()
},
];
let (async_server, app_arc, _server) = MockServarrApi::get()
@@ -649,13 +649,13 @@ mod tests {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
..sonarr_history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
..sonarr_history_item()
},
];
let (async_server, app_arc, _server) = MockServarrApi::get()
@@ -754,13 +754,13 @@ mod tests {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
..sonarr_history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
..sonarr_history_item()
},
];
let (async_server, app_arc, _server) = MockServarrApi::get()
@@ -6,7 +6,7 @@ mod tests {
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::sonarr_network::SonarrEvent;
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
SERIES_JSON, history_item, season, series, torrent_release,
SERIES_JSON, season, series, sonarr_history_item, torrent_release,
};
use mockito::Matcher;
use pretty_assertions::assert_eq;
@@ -278,13 +278,13 @@ mod tests {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
..sonarr_history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
..sonarr_history_item()
},
];
let (mock, app, _server) = MockServarrApi::get()
@@ -390,13 +390,13 @@ mod tests {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
..sonarr_history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
..sonarr_history_item()
},
];
let (mock, app, _server) = MockServarrApi::get()
@@ -10,7 +10,7 @@ mod tests {
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::sonarr_network::SonarrEvent;
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
SERIES_JSON, add_series_search_result, history_item, season, series,
SERIES_JSON, add_series_search_result, season, series, sonarr_history_item,
};
use bimap::BiMap;
use mockito::Matcher;
@@ -457,13 +457,13 @@ mod tests {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
..sonarr_history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
..sonarr_history_item()
},
];
let (async_server, app, _server) = MockServarrApi::get()
@@ -570,13 +570,13 @@ mod tests {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
..sonarr_history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
..sonarr_history_item()
},
];
let (async_server, app, _server) = MockServarrApi::get()
+5 -3
View File
@@ -5,10 +5,12 @@ use serde_json::{Value, json};
use super::{Network, NetworkEvent, NetworkResource};
use crate::{
models::{
servarr_models::{AddRootFolderBody, EditIndexerParams, Language, QualityProfile, Tag},
servarr_models::{
AddRootFolderBody, EditIndexerParams, IndexerSettings, Language, QualityProfile, Tag,
},
sonarr_models::{
AddSeriesBody, DeleteSeriesParams, EditSeriesParams, IndexerSettings,
SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTaskName,
AddSeriesBody, DeleteSeriesParams, EditSeriesParams, SonarrReleaseDownloadBody,
SonarrSerdeable, SonarrTaskName,
},
},
network::RequestMethod,
@@ -5,10 +5,9 @@ pub mod test_utils {
};
use crate::models::sonarr_models::{
AddSeriesSearchResult, AddSeriesSearchResultStatistics, BlocklistItem, DownloadRecord,
DownloadStatus, DownloadsResponse, Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating,
Season, SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType,
SonarrHistoryData, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrTask,
SonarrTaskName,
DownloadStatus, DownloadsResponse, Episode, EpisodeFile, MediaInfo, Rating, Season,
SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, SonarrHistoryData,
SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrTask, SonarrTaskName,
};
use crate::models::{HorizontallyScrollableText, ScrollableText};
use bimap::BiMap;
@@ -204,7 +203,7 @@ pub mod test_utils {
}
}
pub fn history_item() -> SonarrHistoryItem {
pub fn sonarr_history_item() -> SonarrHistoryItem {
SonarrHistoryItem {
id: 1,
source_title: "Test source".into(),
@@ -250,16 +249,6 @@ pub mod test_utils {
}
}
pub fn indexer_settings() -> IndexerSettings {
IndexerSettings {
id: 1,
minimum_age: 1,
retention: 1,
maximum_size: 12345,
rss_sync_interval: 60,
}
}
pub fn language() -> Language {
Language {
id: 1,
@@ -3,11 +3,9 @@ mod test {
use crate::app::App;
use crate::models::servarr_data::sonarr::modals::AddSeriesModal;
use crate::models::servarr_models::{
AddRootFolderBody, EditIndexerParams, Language, QualityProfile, Tag,
};
use crate::models::sonarr_models::{
AddSeriesBody, EditSeriesParams, IndexerSettings, SonarrTaskName,
AddRootFolderBody, EditIndexerParams, IndexerSettings, Language, QualityProfile, Tag,
};
use crate::models::sonarr_models::{AddSeriesBody, EditSeriesParams, SonarrTaskName};
use crate::models::sonarr_models::{DeleteSeriesParams, SonarrSerdeable};
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::tag;
@@ -17,6 +17,7 @@ expression: output
╭─────────────────────────────────── Details ───────────────────────────────────╮
│Source Title: Test source title │
│Event Type: grabbed │
@@ -34,6 +35,4 @@ expression: output
│ │
│ │
│ │
│ │
│ │
╰─────────────────────────────────────────────────────────────────────────────────╯
@@ -17,6 +17,7 @@ expression: output
╭─────────────────────────────────── Details ───────────────────────────────────╮
│Source Title: │
│Event Type: unknown │
@@ -34,6 +35,4 @@ expression: output
│ │
│ │
│ │
│ │
│ │
╰─────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,169 @@
use std::sync::atomic::Ordering;
use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_INDEXER_BLOCKS};
use crate::render_selectable_input_box;
use crate::ui::utils::title_block_centered;
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::popup::Size;
use crate::ui::{DrawUi, draw_popup};
use ratatui::Frame;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
#[cfg(test)]
#[path = "edit_indexer_ui_tests.rs"]
mod edit_indexer_ui_tests;
pub(super) struct EditIndexerUi;
impl DrawUi for EditIndexerUi {
fn accepts(route: Route) -> bool {
let Route::Lidarr(active_lidarr_block, _) = route else {
return false;
};
EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
draw_popup(f, app, draw_edit_indexer_prompt, Size::WideLargePrompt);
}
}
fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let block = title_block_centered("Edit Indexer");
let yes_no_value = app.data.lidarr_data.prompt_confirm;
let selected_block = app.data.lidarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == ActiveLidarrBlock::EditIndexerConfirmPrompt;
let edit_indexer_modal_option = &app.data.lidarr_data.edit_indexer_modal;
let protocol = &app.data.lidarr_data.indexers.current_selection().protocol;
if edit_indexer_modal_option.is_some() {
f.render_widget(block, area);
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
let [settings_area, buttons_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(3)])
.margin(1)
.areas(area);
let [left_side_area, right_side_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.margin(1)
.areas(settings_area);
let [
name_area,
rss_area,
auto_search_area,
interactive_search_area,
priority_area,
] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
])
.areas(left_side_area);
let [url_area, api_key_area, seed_ratio_area, tags_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
])
.areas(right_side_area);
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let priority = edit_indexer_modal.priority.to_string();
let name_input_box = InputBox::new(&edit_indexer_modal.name.text)
.offset(edit_indexer_modal.name.offset.load(Ordering::SeqCst))
.label("Name")
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerNameInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerNameInput);
let url_input_box = InputBox::new(&edit_indexer_modal.url.text)
.offset(edit_indexer_modal.url.offset.load(Ordering::SeqCst))
.label("URL")
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerUrlInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerUrlInput);
let api_key_input_box = InputBox::new(&edit_indexer_modal.api_key.text)
.offset(edit_indexer_modal.api_key.offset.load(Ordering::SeqCst))
.label("API Key")
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerApiKeyInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerApiKeyInput);
let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text)
.offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerTagsInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerTagsInput);
let priority_input_box = InputBox::new(&priority)
.cursor_after_string(false)
.label("Indexer Priority ▴▾")
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerPriorityInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerPriorityInput);
render_selectable_input_box!(name_input_box, f, name_area);
render_selectable_input_box!(url_input_box, f, url_area);
render_selectable_input_box!(api_key_input_box, f, api_key_area);
if protocol == "torrent" {
let seed_ratio_input_box = InputBox::new(&edit_indexer_modal.seed_ratio.text)
.offset(edit_indexer_modal.seed_ratio.offset.load(Ordering::SeqCst))
.label("Seed Ratio")
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerSeedRatioInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerSeedRatioInput);
let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text)
.offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerTagsInput)
.selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerTagsInput);
render_selectable_input_box!(seed_ratio_input_box, f, seed_ratio_area);
render_selectable_input_box!(tags_input_box, f, tags_area);
render_selectable_input_box!(priority_input_box, f, priority_area);
} else {
render_selectable_input_box!(tags_input_box, f, seed_ratio_area);
render_selectable_input_box!(priority_input_box, f, tags_area);
}
let rss_checkbox = Checkbox::new("Enable RSS")
.checked(edit_indexer_modal.enable_rss.unwrap_or_default())
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableRss);
let auto_search_checkbox = Checkbox::new("Enable Automatic Search")
.checked(
edit_indexer_modal
.enable_automatic_search
.unwrap_or_default(),
)
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch);
let interactive_search_checkbox = Checkbox::new("Enable Interactive Search")
.checked(
edit_indexer_modal
.enable_interactive_search
.unwrap_or_default(),
)
.highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch);
let [save_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)])
.flex(Flex::Center)
.areas(buttons_area);
let save_button = Button::default()
.title("Save")
.selected(yes_no_value && highlight_yes_no);
let cancel_button = Button::default()
.title("Cancel")
.selected(!yes_no_value && highlight_yes_no);
f.render_widget(rss_checkbox, rss_area);
f.render_widget(auto_search_checkbox, auto_search_area);
f.render_widget(interactive_search_checkbox, interactive_search_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
}
} else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
}
@@ -0,0 +1,81 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
};
use crate::models::servarr_models::Indexer;
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::indexers::edit_indexer_ui::EditIndexerUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_edit_indexer_ui_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) {
assert!(EditIndexerUi::accepts(active_lidarr_block.into()));
} else {
assert!(!EditIndexerUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use crate::models::servarr_data::lidarr::lidarr_data::EDIT_INDEXER_NZB_SELECTION_BLOCKS;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer;
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use super::*;
#[test]
fn test_edit_indexer_ui_renders_loading() {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.data.lidarr_data.edit_indexer_modal = None;
app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
EditIndexerUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_edit_indexer_ui_renders_edit_torrent_indexer() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
EditIndexerUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_edit_indexer_ui_renders_edit_usenet_indexer() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into());
app.data.lidarr_data.indexers.set_items(vec![Indexer {
protocol: "usenet".into(),
..indexer()
}]);
app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
EditIndexerUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
@@ -0,0 +1,117 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS,
};
use crate::render_selectable_input_box;
use crate::ui::utils::title_block_centered;
use crate::ui::widgets::button::Button;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::popup::Size;
use crate::ui::{DrawUi, draw_popup};
#[cfg(test)]
#[path = "indexer_settings_ui_tests.rs"]
mod indexer_settings_ui_tests;
pub(super) struct IndexerSettingsUi;
impl DrawUi for IndexerSettingsUi {
fn accepts(route: Route) -> bool {
let Route::Lidarr(active_lidarr_block, _) = route else {
return false;
};
INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
draw_popup(f, app, draw_edit_indexer_settings_prompt, Size::LargePrompt);
}
}
fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let block = title_block_centered("Configure All Indexer Settings");
let yes_no_value = app.data.lidarr_data.prompt_confirm;
let selected_block = app.data.lidarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == ActiveLidarrBlock::IndexerSettingsConfirmPrompt;
let indexer_settings_option = &app.data.lidarr_data.indexer_settings;
if indexer_settings_option.is_some() {
f.render_widget(block, area);
let indexer_settings = indexer_settings_option.as_ref().unwrap();
let [
_,
min_age_area,
retention_area,
max_size_area,
rss_sync_area,
_,
buttons_area,
] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3),
])
.margin(1)
.areas(area);
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let min_age = indexer_settings.minimum_age.to_string();
let retention = indexer_settings.retention.to_string();
let max_size = indexer_settings.maximum_size.to_string();
let rss_sync_interval = indexer_settings.rss_sync_interval.to_string();
let min_age_text_box = InputBox::new(&min_age)
.cursor_after_string(false)
.label("Minimum Age (minutes) ▴▾")
.highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsMinimumAgeInput)
.selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsMinimumAgeInput);
let retention_input_box = InputBox::new(&retention)
.cursor_after_string(false)
.label("Retention (days) ▴▾")
.highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsRetentionInput)
.selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsRetentionInput);
let max_size_input_box = InputBox::new(&max_size)
.cursor_after_string(false)
.label("Maximum Size (MB) ▴▾")
.highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsMaximumSizeInput)
.selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsMaximumSizeInput);
let rss_sync_interval_input_box = InputBox::new(&rss_sync_interval)
.cursor_after_string(false)
.label("RSS Sync Interval (minutes) ▴▾")
.highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput)
.selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput);
render_selectable_input_box!(min_age_text_box, f, min_age_area);
render_selectable_input_box!(retention_input_box, f, retention_area);
render_selectable_input_box!(max_size_input_box, f, max_size_area);
render_selectable_input_box!(rss_sync_interval_input_box, f, rss_sync_area);
}
let [save_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)])
.flex(Flex::Center)
.areas(buttons_area);
let save_button = Button::default()
.title("Save")
.selected(yes_no_value && highlight_yes_no);
let cancel_button = Button::default()
.title("Cancel")
.selected(!yes_no_value && highlight_yes_no);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
} else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
}
@@ -0,0 +1,44 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_indexer_settings_ui_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) {
assert!(IndexerSettingsUi::accepts(active_lidarr_block.into()));
} else {
assert!(!IndexerSettingsUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use super::*;
#[test]
fn test_indexer_settings_ui_renders_indexer_settings() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::IndexerSettingsMinimumAgeInput.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexerSettingsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
@@ -0,0 +1,156 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
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::ui::DrawUi;
use crate::ui::lidarr_ui::indexers::IndexersUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_indexers_ui_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!(IndexersUi::accepts(active_lidarr_block.into()));
} else {
assert!(!IndexersUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::{
EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
};
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer;
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use rstest::rstest;
use super::*;
#[test]
fn test_indexers_ui_renders_loading() {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexersUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_indexers_ui_renders_loading_test_results() {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexersUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into());
app.data.lidarr_data.indexer_test_errors = None;
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexersUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_indexers_ui_renders_empty_indexers() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexersUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[rstest]
fn test_indexers_ui_renders(
#[values(
ActiveLidarrBlock::DeleteIndexerPrompt,
ActiveLidarrBlock::Indexers,
ActiveLidarrBlock::TestIndexer
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexersUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("indexers_ui_{active_lidarr_block}"), output);
}
#[test]
fn test_indexers_ui_renders_test_all_over_indexers() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexersUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_indexers_ui_renders_edit_usenet_indexer_over_indexers() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS);
app.data.lidarr_data.indexers.set_items(vec![Indexer {
protocol: "usenet".into(),
..indexer()
}]);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexersUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_indexers_ui_renders_edit_torrent_indexer_over_indexers() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
IndexersUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
+184
View File
@@ -0,0 +1,184 @@
use crate::ui::styles::success_style;
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::text::Text;
use ratatui::widgets::{Cell, Row};
use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, INDEXERS_BLOCKS};
use crate::models::servarr_models::Indexer;
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::indexers::edit_indexer_ui::EditIndexerUi;
use crate::ui::lidarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi;
use crate::ui::lidarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{layout_block_top_border, title_block};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
mod edit_indexer_ui;
mod indexer_settings_ui;
mod test_all_indexers_ui;
#[cfg(test)]
#[path = "indexers_ui_tests.rs"]
mod indexers_ui_tests;
pub(super) struct IndexersUi;
impl DrawUi for IndexersUi {
fn accepts(route: Route) -> bool {
if let Route::Lidarr(active_lidarr_block, _) = route {
return EditIndexerUi::accepts(route)
|| IndexerSettingsUi::accepts(route)
|| TestAllIndexersUi::accepts(route)
|| INDEXERS_BLOCKS.contains(&active_lidarr_block);
}
false
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let route = app.get_current_route();
draw_indexers(f, app, area);
match route {
_ if EditIndexerUi::accepts(route) => EditIndexerUi::draw(f, app, area),
_ if IndexerSettingsUi::accepts(route) => IndexerSettingsUi::draw(f, app, area),
_ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area),
Route::Lidarr(active_lidarr_block, _) => match active_lidarr_block {
ActiveLidarrBlock::TestIndexer => {
if app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none() {
let loading_popup = Popup::new(LoadingBlock::new(
app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none(),
title_block("Testing Indexer"),
))
.size(Size::LargeMessage);
f.render_widget(loading_popup, f.area());
} else {
let popup = {
let result = app
.data
.lidarr_data
.indexer_test_errors
.as_ref()
.expect("Test result is unpopulated");
if !result.is_empty() {
Popup::new(Message::new(result.clone())).size(Size::LargeMessage)
} else {
let message = Message::new("Indexer test succeeded!")
.title("Success")
.style(success_style().bold());
Popup::new(message).size(Size::Message)
}
};
f.render_widget(popup, f.area());
}
}
ActiveLidarrBlock::DeleteIndexerPrompt => {
let prompt = format!(
"Do you really want to delete this indexer: \n{}?",
app
.data
.lidarr_data
.indexers
.current_selection()
.name
.clone()
.unwrap_or_default()
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Delete Indexer")
.prompt(&prompt)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
_ => (),
},
_ => (),
}
}
}
fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let indexers_row_mapping = |indexer: &'_ Indexer| {
let Indexer {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
priority,
tags,
..
} = indexer;
let bool_to_text = |flag: bool| {
if flag {
return Text::from("Enabled").success();
}
Text::from("Disabled").failure()
};
let rss = bool_to_text(*enable_rss);
let automatic_search = bool_to_text(*enable_automatic_search);
let interactive_search = bool_to_text(*enable_interactive_search);
let empty_tag = String::new();
let tags: String = tags
.iter()
.map(|tag_id| {
app
.data
.lidarr_data
.tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap_or(&empty_tag)
.clone()
})
.collect::<Vec<String>>()
.join(", ");
Row::new(vec![
Cell::from(name.clone().unwrap_or_default()),
Cell::from(rss),
Cell::from(automatic_search),
Cell::from(interactive_search),
Cell::from(priority.to_string()),
Cell::from(tags),
])
.primary()
};
let indexers_table = ManagarrTable::new(
Some(&mut app.data.lidarr_data.indexers),
indexers_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.headers([
"Indexer",
"RSS",
"Automatic Search",
"Interactive Search",
"Priority",
"Tags",
])
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(23),
]);
f.render_widget(indexers_table, area);
}
@@ -0,0 +1,42 @@
---
source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs
expression: output
---
╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮
│ │
│ ╭─────────────────────────╮ ╭─────────────────────────╮ │
│ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │
│ ╰─────────────────────────╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable RSS: │ ✔ │ API Key: │someApiKey │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable Automatic Search: │ ✔ │ Seed Ratio: │ratio │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable Interactive Search: │ ✔ │ Tags: │25 │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭─────────────────────────╮ │
│ Indexer Priority ▴▾: │1 │ │
│ ╰─────────────────────────╯ │
│ │
│ │
│ │
│ │
│ ╭───────────────────────────╮╭──────────────────────────╮ │
│ │ Save ││ Cancel │ │
│ ╰───────────────────────────╯╰──────────────────────────╯ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,42 @@
---
source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs
expression: output
---
╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮
│ │
│ ╭─────────────────────────╮ ╭─────────────────────────╮ │
│ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │
│ ╰─────────────────────────╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable RSS: │ ✔ │ API Key: │someApiKey │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable Automatic Search: │ ✔ │ Tags: │25 │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable Interactive Search: │ ✔ │ Indexer Priority ▴▾: │1 │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭───────────────────────────╮╭──────────────────────────╮ │
│ │ Save ││ Cancel │ │
│ ╰───────────────────────────╯╰──────────────────────────╯ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,42 @@
---
source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs
expression: output
---
╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮
│ │
│ │
│ Loading ... │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,40 @@
---
source: src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs
expression: output
---
╭─────────────────── Configure All Indexer Settings ───────────────────╮
│ │
│ │
│ │
│ ╭────────────────────────────────╮ │
│ Minimum Age (minutes) ▴▾: │1 │ │
│ ╰────────────────────────────────╯ │
│ ╭────────────────────────────────╮ │
│ Retention (days) ▴▾: │1 │ │
│ ╰────────────────────────────────╯ │
│ ╭────────────────────────────────╮ │
│ Maximum Size (MB) ▴▾: │12345 │ │
│ ╰────────────────────────────────╯ │
│ ╭────────────────────────────────╮ │
│ RSS Sync Interval (minutes) ▴▾: │60 │ │
│ ╰────────────────────────────────╯ │
│ │
│ │
│ ╭────────────────╮╭────────────────╮ │
│ │ Save ││ Cancel │ │
│ ╰────────────────╯╰────────────────╯ │
╰────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,38 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Indexer RSS Automatic Search Interactive Search Priority Tags
=> Test Indexer Enabled Enabled Enabled 25 alex
╭──────────────────── Delete Indexer ─────────────────────╮
│ Do you really want to delete this indexer: │
│ Test Indexer? │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,7 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Indexer RSS Automatic Search Interactive Search Priority Tags
=> Test Indexer Enabled Enabled Enabled 25 alex
@@ -0,0 +1,35 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Indexer RSS Automatic Search Interactive Search Priority Tags
=> Test Indexer Enabled Enabled Enabled 25 alex
╭─────────────── Error ───────────────╮
│ error │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────╯
@@ -0,0 +1,31 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Indexer RSS Automatic Search Interactive Search Priority Tags
=> Test Indexer Enabled Enabled Enabled 25 alex
╭────────────── Success ──────────────╮
│ Indexer test succeeded! │
│ │
╰───────────────────────────────────────╯
@@ -0,0 +1,42 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Indexer RSS Automatic Search Interactive Search Priority Tags
=> Test Indexer Enabled Enabled Enabled 25 alex
╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮
│ │
│ ╭─────────────────────────╮ ╭─────────────────────────╮ │
│ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │
│ ╰─────────────────────────╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable RSS: │ ✔ │ API Key: │someApiKey │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable Automatic Search: │ ✔ │ Seed Ratio: │ratio │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable Interactive Search: │ ✔ │ Tags: │25 │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭─────────────────────────╮ │
│ Indexer Priority ▴▾: │1 │ │
│ ╰─────────────────────────╯ │
│ │
│ │
│ │
│ │
│ ╭───────────────────────────╮╭──────────────────────────╮ │
│ │ Save ││ Cancel │ │
│ ╰───────────────────────────╯╰──────────────────────────╯ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,42 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Indexer RSS Automatic Search Interactive Search Priority Tags
=> Test Indexer Enabled Enabled Enabled 25 alex
╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮
│ │
│ ╭─────────────────────────╮ ╭─────────────────────────╮ │
│ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │
│ ╰─────────────────────────╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable RSS: │ ✔ │ API Key: │someApiKey │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable Automatic Search: │ ✔ │ Tags: │25 │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ ╭───╮ ╭─────────────────────────╮ │
│ Enable Interactive Search: │ ✔ │ Indexer Priority ▴▾: │1 │ │
│ ╰───╯ ╰─────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭───────────────────────────╮╭──────────────────────────╮ │
│ │ Save ││ Cancel │ │
│ ╰───────────────────────────╯╰──────────────────────────╯ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,5 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,8 @@
---
source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Loading ...

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