Lidarr support #1

Merged
Dark-Alex-17 merged 61 commits from lidarr into main 2026-01-21 21:30:47 +00:00
21 changed files with 701 additions and 100 deletions
Showing only changes of commit 9b4eda6a9d - Show all commits
+2 -1
View File
@@ -7,7 +7,7 @@ use crate::models::Route;
#[path = "lidarr_context_clues_tests.rs"]
mod lidarr_context_clues_tests;
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 7] = [
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 8] = [
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
@@ -20,6 +20,7 @@ pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 7] = [
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.update, "update all"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
@@ -43,6 +43,10 @@ mod tests {
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, "update all")
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
+23
View File
@@ -77,6 +77,7 @@ mod tests {
use tokio::sync::Mutex;
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
use crate::{
app::App,
cli::{
@@ -170,6 +171,28 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::UpdateAllArtists.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_series_command = LidarrCommand::Refresh(LidarrRefreshCommand::AllArtists);
let result = LidarrCliHandler::with(&app_arc, refresh_series_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_toggle_artist_monitoring_command() {
let mut mock_network = MockNetworkTrait::new();
+12
View File
@@ -5,6 +5,7 @@ use clap::{Subcommand, arg};
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
use list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
use tokio::sync::Mutex;
use crate::network::lidarr_network::LidarrEvent;
@@ -15,6 +16,7 @@ use super::{CliCommandHandler, Command};
mod delete_command_handler;
mod get_command_handler;
mod list_command_handler;
mod refresh_command_handler;
#[cfg(test)]
#[path = "lidarr_command_tests.rs"]
@@ -37,6 +39,11 @@ pub enum LidarrCommand {
about = "Commands to list attributes from your Lidarr instance"
)]
List(LidarrListCommand),
#[command(
subcommand,
about = "Commands to refresh the data in your Lidarr instance"
)]
Refresh(LidarrRefreshCommand),
#[command(
about = "Toggle monitoring for the specified artist corresponding to the given artist ID"
)]
@@ -92,6 +99,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
.handle()
.await?
}
LidarrCommand::Refresh(refresh_command) => {
LidarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
.handle()
.await?
}
LidarrCommand::ToggleArtistMonitoring { artist_id } => {
let resp = self
.network
+64
View File
@@ -0,0 +1,64 @@
use std::sync::Arc;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "refresh_command_handler_tests.rs"]
mod refresh_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrRefreshCommand {
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
AllArtists,
}
impl From<LidarrRefreshCommand> for Command {
fn from(value: LidarrRefreshCommand) -> Self {
Command::Lidarr(LidarrCommand::Refresh(value))
}
}
pub(super) struct LidarrRefreshCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
for LidarrRefreshCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrRefreshCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> anyhow::Result<String> {
let result = match self.command {
LidarrRefreshCommand::AllArtists => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateAllArtists.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,72 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::Cli;
use crate::cli::{
Command,
lidarr::{LidarrCommand, refresh_command_handler::LidarrRefreshCommand},
};
use clap::CommandFactory;
#[test]
fn test_lidarr_refresh_command_from() {
let command = LidarrRefreshCommand::AllArtists;
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Refresh(command)));
}
mod cli {
use super::*;
#[test]
fn test_refresh_all_artists_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "all-artists"]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{app::App, cli::lidarr::refresh_command_handler::LidarrRefreshCommandHandler};
use crate::{
cli::{CliCommandHandler, lidarr::refresh_command_handler::LidarrRefreshCommand},
network::lidarr_network::LidarrEvent,
};
use crate::{
models::{Serdeable, lidarr_models::LidarrSerdeable},
network::{MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_refresh_all_artists_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::UpdateAllArtists.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = LidarrRefreshCommand::AllArtists;
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -2,18 +2,18 @@
mod tests {
use std::cmp::Ordering;
use pretty_assertions::assert_str_eq;
use pretty_assertions::{assert_eq, assert_str_eq};
use serde_json::Number;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_modal_absent;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options};
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS};
use crate::network::lidarr_network::LidarrEvent;
use crate::{assert_modal_absent, assert_navigation_popped, assert_navigation_pushed};
#[test]
fn test_library_handler_accepts() {
@@ -267,6 +267,173 @@ mod tests {
assert!(!app.is_routing);
}
#[test]
fn test_update_all_artists_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
LibraryHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Artists,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
}
#[test]
fn test_update_all_artists_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
LibraryHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Artists,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_update_all_artists_prompt_confirm_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.submit.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::UpdateAllArtists
);
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_update_all_artists_prompt_decline_submit() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.submit.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
}
#[test]
fn test_update_all_artists_prompt_esc() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
app.data.lidarr_data.prompt_confirm = true;
LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[test]
fn test_update_all_artists_prompt_left_right() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
LibraryHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[test]
fn test_update_all_artists_prompt_confirm_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
LibraryHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&LidarrEvent::UpdateAllArtists
);
assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into());
}
fn artists_vec() -> Vec<Artist> {
vec![
Artist {
+38 -6
View File
@@ -1,7 +1,7 @@
use crate::{
app::App,
event::Key,
handlers::{KeyEventHandler, handle_clear_errors},
handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle},
matches_key,
models::{
BlockSelectionState,
@@ -108,21 +108,39 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
}
fn handle_left_right_action(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::Artists {
handle_change_tab_left_right_keys(self.app, self.key);
match self.active_lidarr_block {
ActiveLidarrBlock::Artists => handle_change_tab_left_right_keys(self.app, self.key),
ActiveLidarrBlock::UpdateAllArtistsPrompt => handle_prompt_toggle(self.app, self.key),
_ => (),
}
}
fn handle_submit(&mut self) {}
fn handle_submit(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::UpdateAllArtistsPrompt {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
}
self.app.pop_navigation_stack();
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
_ => {
handle_clear_errors(self.app);
}
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_lidarr_block == ActiveLidarrBlock::Artists {
match key {
match self.active_lidarr_block {
ActiveLidarrBlock::Artists => match key {
_ if matches_key!(toggle_monitoring, key) => {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(
@@ -133,12 +151,26 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
.app
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
}
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveLidarrBlock::UpdateAllArtistsPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
@@ -55,7 +55,7 @@ impl<'a> Default for LidarrData<'a> {
quality_profile_map: BiMap::new(),
root_folders: StatefulTable::default(),
selected_block: BlockSelectionState::default(),
start_time: Utc::now(),
start_time: DateTime::default(),
tags_map: BiMap::new(),
version: String::new(),
main_tabs: TabState::new(vec![TabRoute {
@@ -112,19 +112,21 @@ pub enum ActiveLidarrBlock {
DeleteArtistConfirmPrompt,
DeleteArtistToggleDeleteFile,
DeleteArtistToggleAddListExclusion,
SearchArtists,
SearchArtistsError,
FilterArtists,
FilterArtistsError,
SearchArtists,
SearchArtistsError,
UpdateAllArtistsPrompt,
}
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 6] = [
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::Artists,
ActiveLidarrBlock::ArtistsSortPrompt,
ActiveLidarrBlock::SearchArtists,
ActiveLidarrBlock::SearchArtistsError,
ActiveLidarrBlock::FilterArtists,
ActiveLidarrBlock::FilterArtistsError,
ActiveLidarrBlock::SearchArtists,
ActiveLidarrBlock::SearchArtistsError,
ActiveLidarrBlock::UpdateAllArtistsPrompt,
];
pub static DELETE_ARTIST_BLOCKS: [ActiveLidarrBlock; 4] = [
@@ -1,11 +1,15 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::models::{
Route,
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData},
use crate::app::lidarr::lidarr_context_clues::ARTISTS_CONTEXT_CLUES;
use crate::models::servarr_data::lidarr::lidarr_data::{
DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
};
use crate::models::{
BlockSelectionState, Route,
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS, LidarrData},
};
use chrono::{DateTime, Utc};
use pretty_assertions::{assert_eq, assert_str_eq};
#[test]
fn test_from_active_lidarr_block_to_route() {
@@ -36,4 +40,77 @@ mod tests {
assert!(!lidarr_data.delete_artist_files);
assert!(!lidarr_data.add_import_list_exclusion);
}
#[test]
fn test_lidarr_data_default() {
let lidarr_data = LidarrData::default();
assert!(!lidarr_data.add_import_list_exclusion);
assert_is_empty!(lidarr_data.artists);
assert!(!lidarr_data.delete_artist_files);
assert_is_empty!(lidarr_data.disk_space_vec);
assert_is_empty!(lidarr_data.downloads);
assert_is_empty!(lidarr_data.metadata_profile_map);
assert!(!lidarr_data.prompt_confirm);
assert_none!(lidarr_data.prompt_confirm_action);
assert_is_empty!(lidarr_data.quality_profile_map);
assert_is_empty!(lidarr_data.root_folders);
assert_eq!(lidarr_data.selected_block, BlockSelectionState::default());
assert_eq!(lidarr_data.start_time, <DateTime<Utc>>::default());
assert_is_empty!(lidarr_data.tags_map);
assert_is_empty!(lidarr_data.version);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 1);
assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
lidarr_data.main_tabs.tabs[0].route,
ActiveLidarrBlock::Artists.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[0].contextual_help,
&ARTISTS_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[0].config);
}
#[test]
fn test_library_blocks_contains_expected_blocks() {
assert_eq!(LIBRARY_BLOCKS.len(), 7);
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::Artists));
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::ArtistsSortPrompt));
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::SearchArtists));
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistsError));
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::FilterArtists));
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistsError));
assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::UpdateAllArtistsPrompt));
}
#[test]
fn test_delete_artist_blocks_contents() {
assert_eq!(DELETE_ARTIST_BLOCKS.len(), 4);
assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistPrompt));
assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistConfirmPrompt));
assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistToggleDeleteFile));
assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistToggleAddListExclusion));
}
#[test]
fn test_delete_artist_selection_blocks_ordering() {
let mut delete_artist_block_iter = DELETE_ARTIST_SELECTION_BLOCKS.iter();
assert_eq!(
delete_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::DeleteArtistToggleDeleteFile]
);
assert_eq!(
delete_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::DeleteArtistToggleAddListExclusion]
);
assert_eq!(
delete_artist_block_iter.next().unwrap(),
&[ActiveLidarrBlock::DeleteArtistConfirmPrompt]
);
assert_none!(delete_artist_block_iter.next());
}
}
+3
View File
@@ -23,6 +23,7 @@ use crate::{
mod modals_tests;
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct AddSeriesModal {
pub root_folder_list: StatefulList<RootFolder>,
pub monitor_list: StatefulList<SeriesMonitor>,
@@ -130,6 +131,7 @@ impl From<&SonarrData<'_>> for EditIndexerModal {
}
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct EditSeriesModal {
pub series_type_list: StatefulList<SeriesType>,
pub quality_profile_list: StatefulList<String>,
@@ -260,6 +262,7 @@ impl Default for EpisodeDetailsModal {
}
}
#[cfg_attr(test, derive(Debug))]
pub struct SeasonDetailsModal {
pub episodes: StatefulTable<Episode>,
pub episode_files: StatefulTable<EpisodeFile>,
@@ -82,39 +82,39 @@ mod tests {
let sonarr_data = SonarrData::default();
assert!(!sonarr_data.add_list_exclusion);
assert!(sonarr_data.add_searched_series.is_none());
assert!(sonarr_data.add_series_search.is_none());
assert!(sonarr_data.add_series_modal.is_none());
assert!(sonarr_data.blocklist.is_empty());
assert_none!(sonarr_data.add_searched_series);
assert_none!(sonarr_data.add_series_search);
assert_none!(sonarr_data.add_series_modal);
assert_is_empty!(sonarr_data.blocklist);
assert!(!sonarr_data.delete_series_files);
assert!(sonarr_data.downloads.is_empty());
assert!(sonarr_data.disk_space_vec.is_empty());
assert!(sonarr_data.edit_indexer_modal.is_none());
assert!(sonarr_data.edit_root_folder.is_none());
assert!(sonarr_data.edit_series_modal.is_none());
assert!(sonarr_data.history.is_empty());
assert!(sonarr_data.indexers.is_empty());
assert!(sonarr_data.indexer_settings.is_none());
assert!(sonarr_data.indexer_test_errors.is_none());
assert!(sonarr_data.indexer_test_all_results.is_none());
assert!(sonarr_data.language_profiles_map.is_empty());
assert!(sonarr_data.logs.is_empty());
assert!(sonarr_data.log_details.is_empty());
assert_is_empty!(sonarr_data.downloads);
assert_is_empty!(sonarr_data.disk_space_vec);
assert_none!(sonarr_data.edit_indexer_modal);
assert_none!(sonarr_data.edit_root_folder);
assert_none!(sonarr_data.edit_series_modal);
assert_is_empty!(sonarr_data.history);
assert_is_empty!(sonarr_data.indexers);
assert_none!(sonarr_data.indexer_settings);
assert_none!(sonarr_data.indexer_test_errors);
assert_none!(sonarr_data.indexer_test_all_results);
assert_is_empty!(sonarr_data.language_profiles_map);
assert_is_empty!(sonarr_data.logs);
assert_is_empty!(sonarr_data.log_details);
assert!(!sonarr_data.prompt_confirm);
assert!(sonarr_data.prompt_confirm_action.is_none());
assert!(sonarr_data.quality_profile_map.is_empty());
assert!(sonarr_data.queued_events.is_empty());
assert!(sonarr_data.root_folders.is_empty());
assert!(sonarr_data.seasons.is_empty());
assert!(sonarr_data.season_details_modal.is_none());
assert_none!(sonarr_data.prompt_confirm_action);
assert_is_empty!(sonarr_data.quality_profile_map);
assert_is_empty!(sonarr_data.queued_events);
assert_is_empty!(sonarr_data.root_folders);
assert_is_empty!(sonarr_data.seasons);
assert_none!(sonarr_data.season_details_modal);
assert_eq!(sonarr_data.selected_block, BlockSelectionState::default());
assert!(sonarr_data.series.is_empty());
assert!(sonarr_data.series_history.is_none());
assert_is_empty!(sonarr_data.series);
assert_none!(sonarr_data.series_history);
assert_eq!(sonarr_data.start_time, <DateTime<Utc>>::default());
assert!(sonarr_data.tags_map.is_empty());
assert!(sonarr_data.tasks.is_empty());
assert!(sonarr_data.updates.is_empty());
assert!(sonarr_data.version.is_empty());
assert_is_empty!(sonarr_data.tags_map);
assert_is_empty!(sonarr_data.tasks);
assert_is_empty!(sonarr_data.updates);
assert_is_empty!(sonarr_data.version);
assert_eq!(sonarr_data.main_tabs.tabs.len(), 7);
@@ -123,84 +123,77 @@ mod tests {
sonarr_data.main_tabs.tabs[0].route,
ActiveSonarrBlock::Series.into()
);
assert!(sonarr_data.main_tabs.tabs[0].contextual_help.is_some());
assert_eq!(
sonarr_data.main_tabs.tabs[0].contextual_help.unwrap(),
assert_some_eq_x!(
&sonarr_data.main_tabs.tabs[0].contextual_help,
&SERIES_CONTEXT_CLUES
);
assert_eq!(sonarr_data.main_tabs.tabs[0].config, None);
assert_none!(sonarr_data.main_tabs.tabs[0].config);
assert_str_eq!(sonarr_data.main_tabs.tabs[1].title, "Downloads");
assert_eq!(
sonarr_data.main_tabs.tabs[1].route,
ActiveSonarrBlock::Downloads.into()
);
assert!(sonarr_data.main_tabs.tabs[1].contextual_help.is_some());
assert_eq!(
sonarr_data.main_tabs.tabs[1].contextual_help.unwrap(),
assert_some_eq_x!(
&sonarr_data.main_tabs.tabs[1].contextual_help,
&DOWNLOADS_CONTEXT_CLUES
);
assert_eq!(sonarr_data.main_tabs.tabs[1].config, None);
assert_none!(sonarr_data.main_tabs.tabs[1].config);
assert_str_eq!(sonarr_data.main_tabs.tabs[2].title, "Blocklist");
assert_eq!(
sonarr_data.main_tabs.tabs[2].route,
ActiveSonarrBlock::Blocklist.into()
);
assert!(sonarr_data.main_tabs.tabs[2].contextual_help.is_some());
assert_eq!(
sonarr_data.main_tabs.tabs[2].contextual_help.unwrap(),
assert_some_eq_x!(
&sonarr_data.main_tabs.tabs[2].contextual_help,
&BLOCKLIST_CONTEXT_CLUES
);
assert_eq!(sonarr_data.main_tabs.tabs[2].config, None);
assert_none!(sonarr_data.main_tabs.tabs[2].config);
assert_str_eq!(sonarr_data.main_tabs.tabs[3].title, "History");
assert_eq!(
sonarr_data.main_tabs.tabs[3].route,
ActiveSonarrBlock::History.into()
);
assert!(sonarr_data.main_tabs.tabs[3].contextual_help.is_some());
assert_eq!(
sonarr_data.main_tabs.tabs[3].contextual_help.unwrap(),
assert_some_eq_x!(
&sonarr_data.main_tabs.tabs[3].contextual_help,
&HISTORY_CONTEXT_CLUES
);
assert_eq!(sonarr_data.main_tabs.tabs[3].config, None);
assert_none!(sonarr_data.main_tabs.tabs[3].config);
assert_str_eq!(sonarr_data.main_tabs.tabs[4].title, "Root Folders");
assert_eq!(
sonarr_data.main_tabs.tabs[4].route,
ActiveSonarrBlock::RootFolders.into()
);
assert!(sonarr_data.main_tabs.tabs[4].contextual_help.is_some());
assert_eq!(
sonarr_data.main_tabs.tabs[4].contextual_help.unwrap(),
assert_some_eq_x!(
&sonarr_data.main_tabs.tabs[4].contextual_help,
&ROOT_FOLDERS_CONTEXT_CLUES
);
assert_eq!(sonarr_data.main_tabs.tabs[4].config, None);
assert_none!(sonarr_data.main_tabs.tabs[4].config);
assert_str_eq!(sonarr_data.main_tabs.tabs[5].title, "Indexers");
assert_eq!(
sonarr_data.main_tabs.tabs[5].route,
ActiveSonarrBlock::Indexers.into()
);
assert!(sonarr_data.main_tabs.tabs[5].contextual_help.is_some());
assert_eq!(
sonarr_data.main_tabs.tabs[5].contextual_help.unwrap(),
assert_some_eq_x!(
&sonarr_data.main_tabs.tabs[5].contextual_help,
&INDEXERS_CONTEXT_CLUES
);
assert_eq!(sonarr_data.main_tabs.tabs[5].config, None);
assert_none!(sonarr_data.main_tabs.tabs[5].config);
assert_str_eq!(sonarr_data.main_tabs.tabs[6].title, "System");
assert_eq!(
sonarr_data.main_tabs.tabs[6].route,
ActiveSonarrBlock::System.into()
);
assert!(sonarr_data.main_tabs.tabs[6].contextual_help.is_some());
assert_eq!(
sonarr_data.main_tabs.tabs[6].contextual_help.unwrap(),
assert_some_eq_x!(
&sonarr_data.main_tabs.tabs[6].contextual_help,
&SYSTEM_CONTEXT_CLUES
);
assert_eq!(sonarr_data.main_tabs.tabs[6].config, None);
assert_none!(sonarr_data.main_tabs.tabs[6].config);
assert_eq!(sonarr_data.series_info_tabs.tabs.len(), 2);
@@ -209,36 +202,22 @@ mod tests {
sonarr_data.series_info_tabs.tabs[0].route,
ActiveSonarrBlock::SeriesDetails.into()
);
assert!(
sonarr_data.series_info_tabs.tabs[0]
.contextual_help
.is_some()
);
assert_eq!(
sonarr_data.series_info_tabs.tabs[0]
.contextual_help
.unwrap(),
assert_some_eq_x!(
&sonarr_data.series_info_tabs.tabs[0].contextual_help,
&SERIES_DETAILS_CONTEXT_CLUES
);
assert_eq!(sonarr_data.series_info_tabs.tabs[0].config, None);
assert_none!(sonarr_data.series_info_tabs.tabs[0].config);
assert_str_eq!(sonarr_data.series_info_tabs.tabs[1].title, "History");
assert_eq!(
sonarr_data.series_info_tabs.tabs[1].route,
ActiveSonarrBlock::SeriesHistory.into()
);
assert!(
sonarr_data.series_info_tabs.tabs[1]
.contextual_help
.is_some()
);
assert_eq!(
sonarr_data.series_info_tabs.tabs[1]
.contextual_help
.unwrap(),
assert_some_eq_x!(
&sonarr_data.series_info_tabs.tabs[1].contextual_help,
&SERIES_HISTORY_CONTEXT_CLUES
);
assert_eq!(sonarr_data.series_info_tabs.tabs[1].config, None);
assert_none!(sonarr_data.series_info_tabs.tabs[1].config);
}
}
@@ -162,4 +162,26 @@ mod tests {
get_mock.assert_async().await;
put_mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_update_all_artists_event() {
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "RefreshArtist"
}))
.returns(json!({}))
.build_for(LidarrEvent::UpdateAllArtists)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::UpdateAllArtists)
.await
.is_ok()
);
mock.assert_async().await;
}
}
+17
View File
@@ -5,6 +5,7 @@ use serde_json::{Value, json};
use crate::models::Route;
use crate::models::lidarr_models::{Artist, DeleteArtistParams};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_models::CommandBody;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
@@ -151,4 +152,20 @@ impl Network<'_, '_> {
}
}
}
pub(in crate::network::lidarr_network) async fn update_all_artists(&mut self) -> Result<Value> {
info!("Updating all artists");
let event = LidarrEvent::UpdateAllArtists;
let body = CommandBody {
name: "RefreshArtist".to_owned(),
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
}
@@ -27,6 +27,11 @@ mod tests {
assert_str_eq!(event.resource(), "/config/host");
}
#[rstest]
fn test_resource_command(#[values(LidarrEvent::UpdateAllArtists)] event: LidarrEvent) {
assert_str_eq!(event.resource(), "/command");
}
#[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
#[case(LidarrEvent::GetDownloads(500), "/queue")]
+3
View File
@@ -31,6 +31,7 @@ pub enum LidarrEvent {
HealthCheck,
ListArtists,
ToggleArtistMonitoring(i64),
UpdateAllArtists,
}
impl NetworkResource for LidarrEvent {
@@ -43,6 +44,7 @@ impl NetworkResource for LidarrEvent {
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) => "/queue",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::UpdateAllArtists => "/command",
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
LidarrEvent::GetQualityProfiles => "/qualityprofile",
LidarrEvent::GetRootFolders => "/rootfolder",
@@ -108,6 +110,7 @@ impl Network<'_, '_> {
.toggle_artist_monitoring(artist_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from),
}
}
@@ -247,5 +247,18 @@ mod tests {
insta::assert_snapshot!(output);
}
#[test]
fn test_library_ui_renders_update_all_artists_prompt() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
LibraryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
+18 -2
View File
@@ -6,6 +6,10 @@ use ratatui::{
};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::{
confirmation_prompt::ConfirmationPrompt,
popup::{Popup, Size},
};
use crate::utils::convert_to_gb;
use crate::{
app::App,
@@ -42,8 +46,20 @@ impl DrawUi for LibraryUi {
let route = app.get_current_route();
draw_library(f, app, area);
if DeleteArtistUi::accepts(route) {
DeleteArtistUi::draw(f, app, area);
match route {
_ if DeleteArtistUi::accepts(route) => DeleteArtistUi::draw(f, app, area),
Route::Lidarr(ActiveLidarrBlock::UpdateAllArtistsPrompt, _) => {
let confirmation_prompt = ConfirmationPrompt::new()
.title("Update All Artists")
.prompt("Do you want to update info and scan your disks for all of your artists?")
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
_ => (),
}
}
}
@@ -0,0 +1,38 @@
---
source: src/ui/lidarr_ui/library/library_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Continuing 0 0.00 GB
╭────────────────── Update All Artists ───────────────────╮
│ Do you want to update info and scan your disks for all of │
│ your artists? │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -382,5 +382,18 @@ mod tests {
insta::assert_snapshot!(output);
}
#[test]
fn test_library_ui_renders_update_all_series_prompt() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::UpdateAllSeriesPrompt.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
LibraryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
@@ -0,0 +1,38 @@
---
source: src/ui/sonarr_ui/library/library_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title ▼ Year Network Status Rating Type Quality Profile Language Size Monitored Tags
=> Test 2022 HBO Continuin TV-MA Standard Bluray-1080p English 59.51 GB 🏷
╭─────────────────── Update All Series ───────────────────╮
│ Do you want to update info and scan your disks for all of │
│ your series? │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯