From 34157ef32f9a3e2a54731e8619328855b8066b8a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 20 Nov 2024 19:33:40 -0700 Subject: [PATCH] feat(cli): Added a spinner to the CLI for long running commands like fetching releases --- Cargo.lock | 55 +++++++++ Cargo.toml | 1 + README.md | 8 +- src/cli/mod.rs | 25 +--- src/cli/radarr/add_command_handler.rs | 30 +++-- src/cli/radarr/delete_command_handler.rs | 48 +++++--- src/cli/radarr/edit_command_handler.rs | 51 ++++---- src/cli/radarr/get_command_handler.rs | 45 +++++-- src/cli/radarr/list_command_handler.rs | 87 ++++++++++---- src/cli/radarr/mod.rs | 57 +++++++-- src/cli/radarr/refresh_command_handler.rs | 33 ++++-- src/cli/sonarr/delete_command_handler.rs | 18 +-- src/cli/sonarr/get_command_handler.rs | 45 +++++-- src/cli/sonarr/list_command_handler.rs | 69 ++++++++--- src/cli/sonarr/mod.rs | 32 +++-- src/main.rs | 99 ++++------------ src/models/sonarr_models.rs | 82 +++++++++++-- src/models/sonarr_models_tests.rs | 66 ++++++++++- src/utils.rs | 137 +++++++++++++++++++++- 19 files changed, 717 insertions(+), 271 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40c816b..ff134d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,6 +370,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width 0.1.14", + "windows-sys 0.52.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -565,6 +578,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1117,6 +1136,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.0", + "web-time", +] + [[package]] name = "indoc" version = "2.0.5" @@ -1289,6 +1321,7 @@ dependencies = [ "derivative", "dirs-next", "human-panic", + "indicatif", "indoc", "itertools", "log", @@ -1458,6 +1491,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.36.5" @@ -1596,6 +1635,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -2675,6 +2720,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 3563627..7c48c4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ colored = "2.1.0" async-trait = "0.1.83" dirs-next = "2.0.0" managarr-tree-widget = "0.24.0" +indicatif = "0.17.9" [dev-dependencies] assert_cmd = "2.0.16" diff --git a/README.md b/README.md index aa47fba..597f2b0 100644 --- a/README.md +++ b/README.md @@ -236,9 +236,11 @@ tautulli: ## Environment Variables Managarr supports using environment variables on startup so you don't have to always specify certain flags: -| Variable | Description | Equivalent Flag | -| --------------------------------------- | -------------------------------- | -------------------------------- | -| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` | +| Variable | Description | Equivalent Flag | +|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------| +| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` | +| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` | +|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------| ## Track My Progress for the Beta release (With Sonarr Support!) Progress for the beta release can be followed on my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 1178276..5eb9c37 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -42,15 +42,15 @@ pub enum Command { pub trait CliCommandHandler<'a, 'b, T: Into> { fn with(app: &'a Arc>>, command: T, network: &'a mut dyn NetworkTrait) -> Self; - async fn handle(self) -> Result<()>; + async fn handle(self) -> Result; } pub(crate) async fn handle_command( app: &Arc>>, command: Command, network: &mut dyn NetworkTrait, -) -> Result<()> { - match command { +) -> Result { + let result = match command { Command::Radarr(radarr_command) => { RadarrCliHandler::with(app, radarr_command, network) .handle() @@ -61,10 +61,10 @@ pub(crate) async fn handle_command( .handle() .await? } - _ => (), - } + _ => String::new(), + }; - Ok(()) + Ok(result) } #[inline] @@ -88,16 +88,3 @@ pub fn mutex_flags_or_default(positive: bool, negative: bool, default_value: boo default_value } } - -#[macro_export] -macro_rules! execute_network_event { - ($self:ident, $event:expr) => { - let resp = $self.network.handle_network_event($event.into()).await?; - let json = serde_json::to_string_pretty(&resp)?; - println!("{}", json); - }; - ($self:ident, $event:expr, $happy_output:expr) => { - $self.network.handle_network_event($event.into()).await?; - println!("{}", $happy_output); - }; -} diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs index 1e26963..007306f 100644 --- a/src/cli/radarr/add_command_handler.rs +++ b/src/cli/radarr/add_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - execute_network_event, models::radarr_models::{AddMovieBody, AddOptions, MinimumAvailability, Monitor}, network::{radarr_network::RadarrEvent, NetworkTrait}, }; @@ -106,8 +105,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { RadarrAddCommand::Movie { tmdb_id, root_folder_path, @@ -131,19 +130,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan search_for_movie: !no_search_for_movie, }, }; - execute_network_event!(self, RadarrEvent::AddMovie(Some(body))); + let resp = self + .network + .handle_network_event((RadarrEvent::AddMovie(Some(body))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrAddCommand::RootFolder { root_folder_path } => { - execute_network_event!( - self, - RadarrEvent::AddRootFolder(Some(root_folder_path.clone())) - ); + let resp = self + .network + .handle_network_event((RadarrEvent::AddRootFolder(Some(root_folder_path.clone()))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrAddCommand::Tag { name } => { - execute_network_event!(self, RadarrEvent::AddTag(name.clone())); + let resp = self + .network + .handle_network_event((RadarrEvent::AddTag(name.clone())).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/radarr/delete_command_handler.rs b/src/cli/radarr/delete_command_handler.rs index db26775..8568c88 100644 --- a/src/cli/radarr/delete_command_handler.rs +++ b/src/cli/radarr/delete_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - execute_network_event, models::radarr_models::DeleteMovieParams, network::{radarr_network::RadarrEvent, NetworkTrait}, }; @@ -85,19 +84,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => { - execute_network_event!( - self, - RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)) - ); + let resp = self + .network + .handle_network_event((RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrDeleteCommand::Download { download_id } => { - execute_network_event!(self, RadarrEvent::DeleteDownload(Some(download_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::DeleteDownload(Some(download_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrDeleteCommand::Indexer { indexer_id } => { - execute_network_event!(self, RadarrEvent::DeleteIndexer(Some(indexer_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::DeleteIndexer(Some(indexer_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrDeleteCommand::Movie { movie_id, @@ -109,16 +117,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm delete_movie_files: delete_files_from_disk, add_list_exclusion, }; - execute_network_event!(self, RadarrEvent::DeleteMovie(Some(delete_movie_params))); + let resp = self + .network + .handle_network_event((RadarrEvent::DeleteMovie(Some(delete_movie_params))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrDeleteCommand::RootFolder { root_folder_id } => { - execute_network_event!(self, RadarrEvent::DeleteRootFolder(Some(root_folder_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::DeleteRootFolder(Some(root_folder_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrDeleteCommand::Tag { tag_id } => { - execute_network_event!(self, RadarrEvent::DeleteTag(tag_id)); + let resp = self + .network + .handle_network_event((RadarrEvent::DeleteTag(tag_id)).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/radarr/edit_command_handler.rs b/src/cli/radarr/edit_command_handler.rs index 666828c..f3396c9 100644 --- a/src/cli/radarr/edit_command_handler.rs +++ b/src/cli/radarr/edit_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command}, - execute_network_event, models::{ radarr_models::{ EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings, @@ -339,8 +338,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { RadarrEditCommand::AllIndexerSettings { allow_hardcoded_subs, disable_allow_hardcoded_subs, @@ -389,11 +388,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH }) .into(), }; - execute_network_event!( - self, - RadarrEvent::EditAllIndexerSettings(Some(params)), - "All indexer settings updated" - ); + self + .network + .handle_network_event((RadarrEvent::EditAllIndexerSettings(Some(params))).into()) + .await?; + "All indexer settings updated".to_owned() + } else { + String::new() } } RadarrEditCommand::Collection { @@ -417,11 +418,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH root_folder_path, search_on_add: search_on_add_value, }; - execute_network_event!( - self, - RadarrEvent::EditCollection(Some(edit_collection_params)), - "Collection Updated" - ); + self + .network + .handle_network_event((RadarrEvent::EditCollection(Some(edit_collection_params))).into()) + .await?; + "Collection updated".to_owned() } RadarrEditCommand::Indexer { indexer_id, @@ -458,11 +459,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH clear_tags, }; - execute_network_event!( - self, - RadarrEvent::EditIndexer(Some(edit_indexer_params)), - "Indexer updated" - ); + self + .network + .handle_network_event((RadarrEvent::EditIndexer(Some(edit_indexer_params))).into()) + .await?; + "Indexer updated".to_owned() } RadarrEditCommand::Movie { movie_id, @@ -485,14 +486,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH clear_tags, }; - execute_network_event!( - self, - RadarrEvent::EditMovie(Some(edit_movie_params)), - "Movie updated" - ); + self + .network + .handle_network_event((RadarrEvent::EditMovie(Some(edit_movie_params))).into()) + .await?; + "Movie Updated".to_owned() } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/radarr/get_command_handler.rs b/src/cli/radarr/get_command_handler.rs index 1c398ab..4df2595 100644 --- a/src/cli/radarr/get_command_handler.rs +++ b/src/cli/radarr/get_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - execute_network_event, network::{radarr_network::RadarrEvent, NetworkTrait}, }; @@ -72,28 +71,52 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { RadarrGetCommand::AllIndexerSettings => { - execute_network_event!(self, RadarrEvent::GetAllIndexerSettings); + let resp = self + .network + .handle_network_event((RadarrEvent::GetAllIndexerSettings).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrGetCommand::HostConfig => { - execute_network_event!(self, RadarrEvent::GetHostConfig); + let resp = self + .network + .handle_network_event((RadarrEvent::GetHostConfig).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrGetCommand::MovieDetails { movie_id } => { - execute_network_event!(self, RadarrEvent::GetMovieDetails(Some(movie_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::GetMovieDetails(Some(movie_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrGetCommand::MovieHistory { movie_id } => { - execute_network_event!(self, RadarrEvent::GetMovieHistory(Some(movie_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::GetMovieHistory(Some(movie_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrGetCommand::SecurityConfig => { - execute_network_event!(self, RadarrEvent::GetSecurityConfig); + let resp = self + .network + .handle_network_event((RadarrEvent::GetSecurityConfig).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrGetCommand::SystemStatus => { - execute_network_event!(self, RadarrEvent::GetStatus); + let resp = self + .network + .handle_network_event((RadarrEvent::GetStatus).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs index e536d7f..0fc4dfb 100644 --- a/src/cli/radarr/list_command_handler.rs +++ b/src/cli/radarr/list_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - execute_network_event, network::{radarr_network::RadarrEvent, NetworkTrait}, }; @@ -87,19 +86,35 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { RadarrListCommand::Blocklist => { - execute_network_event!(self, RadarrEvent::GetBlocklist); + let resp = self + .network + .handle_network_event((RadarrEvent::GetBlocklist).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::Collections => { - execute_network_event!(self, RadarrEvent::GetCollections); + let resp = self + .network + .handle_network_event((RadarrEvent::GetCollections).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::Downloads => { - execute_network_event!(self, RadarrEvent::GetDownloads); + let resp = self + .network + .handle_network_event((RadarrEvent::GetDownloads).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::Indexers => { - execute_network_event!(self, RadarrEvent::GetIndexers); + let resp = self + .network + .handle_network_event((RadarrEvent::GetIndexers).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::Logs { events, @@ -113,39 +128,69 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH if output_in_log_format { let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone(); - let json = serde_json::to_string_pretty(&log_lines)?; - println!("{}", json); + serde_json::to_string_pretty(&log_lines)? } else { - let json = serde_json::to_string_pretty(&logs)?; - println!("{}", json); + serde_json::to_string_pretty(&logs)? } } RadarrListCommand::Movies => { - execute_network_event!(self, RadarrEvent::GetMovies); + let resp = self + .network + .handle_network_event((RadarrEvent::GetMovies).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::MovieCredits { movie_id } => { - execute_network_event!(self, RadarrEvent::GetMovieCredits(Some(movie_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::GetMovieCredits(Some(movie_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::QualityProfiles => { - execute_network_event!(self, RadarrEvent::GetQualityProfiles); + let resp = self + .network + .handle_network_event((RadarrEvent::GetQualityProfiles).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::QueuedEvents => { - execute_network_event!(self, RadarrEvent::GetQueuedEvents); + let resp = self + .network + .handle_network_event((RadarrEvent::GetQueuedEvents).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::RootFolders => { - execute_network_event!(self, RadarrEvent::GetRootFolders); + let resp = self + .network + .handle_network_event((RadarrEvent::GetRootFolders).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::Tags => { - execute_network_event!(self, RadarrEvent::GetTags); + let resp = self + .network + .handle_network_event((RadarrEvent::GetTags).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::Tasks => { - execute_network_event!(self, RadarrEvent::GetTasks); + let resp = self + .network + .handle_network_event((RadarrEvent::GetTasks).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrListCommand::Updates => { - execute_network_event!(self, RadarrEvent::GetUpdates); + let resp = self + .network + .handle_network_event((RadarrEvent::GetUpdates).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index 96c7938..2bb9e34 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -12,7 +12,6 @@ use tokio::sync::Mutex; use crate::app::App; use crate::cli::CliCommandHandler; -use crate::execute_network_event; use crate::models::radarr_models::{ReleaseDownloadBody, TaskName}; use crate::network::radarr_network::RadarrEvent; use crate::network::NetworkTrait; @@ -155,8 +154,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { RadarrCommand::Add(add_command) => { RadarrAddCommandHandler::with(self.app, add_command, self.network) .handle() @@ -192,7 +191,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' .network .handle_network_event(RadarrEvent::GetBlocklist.into()) .await?; - execute_network_event!(self, RadarrEvent::ClearBlocklist); + let resp = self + .network + .handle_network_event((RadarrEvent::ClearBlocklist).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrCommand::DownloadRelease { guid, @@ -204,29 +207,57 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' indexer_id, movie_id, }; - execute_network_event!(self, RadarrEvent::DownloadRelease(Some(params))); + let resp = self + .network + .handle_network_event((RadarrEvent::DownloadRelease(Some(params))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrCommand::ManualSearch { movie_id } => { println!("Searching for releases. This may take a minute..."); - execute_network_event!(self, RadarrEvent::GetReleases(Some(movie_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::GetReleases(Some(movie_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrCommand::SearchNewMovie { query } => { - execute_network_event!(self, RadarrEvent::SearchNewMovie(Some(query))); + let resp = self + .network + .handle_network_event((RadarrEvent::SearchNewMovie(Some(query))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrCommand::StartTask { task_name } => { - execute_network_event!(self, RadarrEvent::StartTask(Some(task_name))); + let resp = self + .network + .handle_network_event((RadarrEvent::StartTask(Some(task_name))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrCommand::TestIndexer { indexer_id } => { - execute_network_event!(self, RadarrEvent::TestIndexer(Some(indexer_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::TestIndexer(Some(indexer_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrCommand::TestAllIndexers => { - execute_network_event!(self, RadarrEvent::TestAllIndexers); + let resp = self + .network + .handle_network_event((RadarrEvent::TestAllIndexers).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrCommand::TriggerAutomaticSearch { movie_id } => { - execute_network_event!(self, RadarrEvent::TriggerAutomaticSearch(Some(movie_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::TriggerAutomaticSearch(Some(movie_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/radarr/refresh_command_handler.rs b/src/cli/radarr/refresh_command_handler.rs index 5bb0e73..201be01 100644 --- a/src/cli/radarr/refresh_command_handler.rs +++ b/src/cli/radarr/refresh_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - execute_network_event, network::{radarr_network::RadarrEvent, NetworkTrait}, }; @@ -63,22 +62,38 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand> } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { RadarrRefreshCommand::AllMovies => { - execute_network_event!(self, RadarrEvent::UpdateAllMovies); + let resp = self + .network + .handle_network_event((RadarrEvent::UpdateAllMovies).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrRefreshCommand::Collections => { - execute_network_event!(self, RadarrEvent::UpdateCollections); + let resp = self + .network + .handle_network_event((RadarrEvent::UpdateCollections).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrRefreshCommand::Downloads => { - execute_network_event!(self, RadarrEvent::UpdateDownloads); + let resp = self + .network + .handle_network_event((RadarrEvent::UpdateDownloads).into()) + .await?; + serde_json::to_string_pretty(&resp)? } RadarrRefreshCommand::Movie { movie_id } => { - execute_network_event!(self, RadarrEvent::UpdateAndScan(Some(movie_id))); + let resp = self + .network + .handle_network_event((RadarrEvent::UpdateAndScan(Some(movie_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/sonarr/delete_command_handler.rs b/src/cli/sonarr/delete_command_handler.rs index cc73e74..2bf8991 100644 --- a/src/cli/sonarr/delete_command_handler.rs +++ b/src/cli/sonarr/delete_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - execute_network_event, network::{sonarr_network::SonarrEvent, NetworkTrait}, }; @@ -55,16 +54,17 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let resp = match self.command { SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => { - execute_network_event!( - self, - SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)) - ); + let resp = self + .network + .handle_network_event((SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(resp) } } diff --git a/src/cli/sonarr/get_command_handler.rs b/src/cli/sonarr/get_command_handler.rs index 7af1ee6..091ef87 100644 --- a/src/cli/sonarr/get_command_handler.rs +++ b/src/cli/sonarr/get_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - execute_network_event, network::{sonarr_network::SonarrEvent, NetworkTrait}, }; @@ -72,28 +71,52 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHan } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { SonarrGetCommand::AllIndexerSettings => { - execute_network_event!(self, SonarrEvent::GetAllIndexerSettings); + let resp = self + .network + .handle_network_event((SonarrEvent::GetAllIndexerSettings).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrGetCommand::EpisodeDetails { episode_id } => { - execute_network_event!(self, SonarrEvent::GetEpisodeDetails(Some(episode_id))); + let resp = self + .network + .handle_network_event((SonarrEvent::GetEpisodeDetails(Some(episode_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrGetCommand::HostConfig => { - execute_network_event!(self, SonarrEvent::GetHostConfig); + let resp = self + .network + .handle_network_event((SonarrEvent::GetHostConfig).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrGetCommand::SecurityConfig => { - execute_network_event!(self, SonarrEvent::GetSecurityConfig); + let resp = self + .network + .handle_network_event((SonarrEvent::GetSecurityConfig).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrGetCommand::SeriesDetails { series_id } => { - execute_network_event!(self, SonarrEvent::GetSeriesDetails(Some(series_id))); + let resp = self + .network + .handle_network_event((SonarrEvent::GetSeriesDetails(Some(series_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrGetCommand::SystemStatus => { - execute_network_event!(self, SonarrEvent::GetStatus); + let resp = self + .network + .handle_network_event((SonarrEvent::GetStatus).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index b95ac8d..567d2db 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -7,7 +7,6 @@ use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - execute_network_event, network::{sonarr_network::SonarrEvent, NetworkTrait}, }; @@ -91,22 +90,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { SonarrListCommand::Blocklist => { - execute_network_event!(self, SonarrEvent::GetBlocklist); + let resp = self + .network + .handle_network_event((SonarrEvent::GetBlocklist).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrListCommand::Downloads => { - execute_network_event!(self, SonarrEvent::GetDownloads); + let resp = self + .network + .handle_network_event((SonarrEvent::GetDownloads).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrListCommand::Episodes { series_id } => { - execute_network_event!(self, SonarrEvent::GetEpisodes(Some(series_id))); + let resp = self + .network + .handle_network_event((SonarrEvent::GetEpisodes(Some(series_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrListCommand::History { events: items } => { - execute_network_event!(self, SonarrEvent::GetHistory(Some(items))); + let resp = self + .network + .handle_network_event((SonarrEvent::GetHistory(Some(items))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrListCommand::Indexers => { - execute_network_event!(self, SonarrEvent::GetIndexers); + let resp = self + .network + .handle_network_event((SonarrEvent::GetIndexers).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrListCommand::Logs { events, @@ -120,27 +139,41 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH if output_in_log_format { let log_lines = self.app.lock().await.data.sonarr_data.logs.items.clone(); - let json = serde_json::to_string_pretty(&log_lines)?; - println!("{}", json); + serde_json::to_string_pretty(&log_lines)? } else { - let json = serde_json::to_string_pretty(&logs)?; - println!("{}", json); + serde_json::to_string_pretty(&logs)? } } SonarrListCommand::QualityProfiles => { - execute_network_event!(self, SonarrEvent::GetQualityProfiles); + let resp = self + .network + .handle_network_event((SonarrEvent::GetQualityProfiles).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrListCommand::QueuedEvents => { - execute_network_event!(self, SonarrEvent::GetQueuedEvents); + let resp = self + .network + .handle_network_event((SonarrEvent::GetQueuedEvents).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrListCommand::Series => { - execute_network_event!(self, SonarrEvent::ListSeries); + let resp = self + .network + .handle_network_event((SonarrEvent::ListSeries).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrListCommand::SeriesHistory { series_id } => { - execute_network_event!(self, SonarrEvent::GetSeriesHistory(Some(series_id))); + let resp = self + .network + .handle_network_event((SonarrEvent::GetSeriesHistory(Some(series_id))).into()) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 693ae01..52239a7 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -9,7 +9,6 @@ use tokio::sync::Mutex; use crate::{ app::App, - execute_network_event, network::{sonarr_network::SonarrEvent, NetworkTrait}, }; @@ -91,8 +90,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' } } - async fn handle(self) -> Result<()> { - match self.command { + async fn handle(self) -> Result { + let result = match self.command { SonarrCommand::Delete(delete_command) => { SonarrDeleteCommandHandler::with(self.app, delete_command, self.network) .handle() @@ -113,24 +112,35 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .network .handle_network_event(SonarrEvent::GetBlocklist.into()) .await?; - execute_network_event!(self, SonarrEvent::ClearBlocklist); + let resp = self + .network + .handle_network_event(SonarrEvent::ClearBlocklist.into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrCommand::ManualEpisodeSearch { episode_id } => { println!("Searching for episode releases. This may take a minute..."); - execute_network_event!(self, SonarrEvent::GetEpisodeReleases(Some(episode_id))); + let resp = self + .network + .handle_network_event(SonarrEvent::GetEpisodeReleases(Some(episode_id)).into()) + .await?; + serde_json::to_string_pretty(&resp)? } SonarrCommand::ManualSeasonSearch { series_id, season_number, } => { println!("Searching for season releases. This may take a minute..."); - execute_network_event!( - self, - SonarrEvent::GetSeasonReleases(Some((series_id, season_number))) - ); + let resp = self + .network + .handle_network_event( + SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(), + ) + .await?; + serde_json::to_string_pretty(&resp)? } - } + }; - Ok(()) + Ok(result) } } diff --git a/src/main.rs b/src/main.rs index b6ece85..299ac2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,13 @@ #![warn(rust_2018_idioms)] -use std::fs::{self, File}; -use std::io::BufReader; +use anyhow::Result; use std::panic::PanicHookInfo; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::time::Duration; use std::{io, panic, process}; -use anyhow::anyhow; -use anyhow::Result; -use app::{log_and_print_error, AppConfig}; -use clap::{ - command, crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser, -}; +use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser}; use clap_complete::generate; use colored::Colorize; use crossterm::execute; @@ -25,14 +18,17 @@ use log::{error, warn}; use network::NetworkTrait; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; -use reqwest::{Certificate, Client}; +use reqwest::Client; use tokio::select; use tokio::sync::mpsc::Receiver; use tokio::sync::{mpsc, Mutex}; use tokio_util::sync::CancellationToken; -use utils::tail_logs; +use utils::{ + build_network_client, load_config, render_spinner, start_cli_no_spinner, start_cli_with_spinner, + tail_logs, +}; -use crate::app::App; +use crate::app::{App, AppConfig}; use crate::cli::Command; use crate::event::input_event::{Events, InputEvent}; use crate::event::Key; @@ -67,6 +63,13 @@ mod utils; struct Cli { #[command(subcommand)] command: Option, + #[arg( + long, + global = true, + env = "MANAGARR_DISABLE_SPINNER", + help = "Disable the spinner (can sometimes make parsing output challenging)" + )] + disable_spinner: bool, #[arg( long, global = true, @@ -91,6 +94,7 @@ async fn main() -> Result<()> { } else { confy::load("managarr", "config")? }; + let spinner_disabled = args.disable_spinner; config.validate(); let reqwest_client = build_network_client(&config); let (sync_network_tx, sync_network_rx) = mpsc::channel(500); @@ -113,14 +117,10 @@ async fn main() -> Result<()> { match args.command { Some(command) => match command { Command::Radarr(_) | Command::Sonarr(_) => { - config.verify_config_present_for_cli(&command); - app.lock().await.cli_mode = true; - let app_nw = Arc::clone(&app); - let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); - - if let Err(e) = cli::handle_command(&app, command, &mut network).await { - eprintln!("error: {}", e.to_string().red()); - process::exit(1); + if spinner_disabled { + start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await; + } else { + start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await; } } Command::Completions { shell } => { @@ -237,65 +237,6 @@ fn panic_hook(info: &PanicHookInfo<'_>) { .unwrap(); } -fn load_config(path: &str) -> Result { - let file = File::open(path).map_err(|e| anyhow!(e))?; - let reader = BufReader::new(file); - let config = serde_yaml::from_reader(reader)?; - Ok(config) -} - -fn build_network_client(config: &AppConfig) -> Client { - let mut client_builder = Client::builder() - .pool_max_idle_per_host(10) - .http2_keep_alive_interval(Duration::from_secs(5)) - .tcp_keepalive(Duration::from_secs(5)); - - if let Some(radarr_config) = &config.radarr { - if let Some(ref cert_path) = &radarr_config.ssl_cert_path { - let cert = create_cert(cert_path, "Radarr"); - client_builder = client_builder.add_root_certificate(cert); - } - } - - if let Some(sonarr_config) = &config.sonarr { - if let Some(ref cert_path) = &sonarr_config.ssl_cert_path { - let cert = create_cert(cert_path, "Sonarr"); - client_builder = client_builder.add_root_certificate(cert); - } - } - - match client_builder.build() { - Ok(client) => client, - Err(e) => { - error!("Unable to create reqwest client: {}", e); - eprintln!("error: {}", e.to_string().red()); - process::exit(1); - } - } -} - -fn create_cert(cert_path: &String, servarr_name: &str) -> Certificate { - match fs::read(cert_path) { - Ok(cert) => match Certificate::from_pem(&cert) { - Ok(certificate) => certificate, - Err(_) => { - log_and_print_error(format!( - "Unable to read the specified {} SSL certificate", - servarr_name - )); - process::exit(1); - } - }, - Err(_) => { - log_and_print_error(format!( - "Unable to open specified {} SSL certificate", - servarr_name - )); - process::exit(1); - } - } -} - #[cfg(not(debug_assertions))] fn panic_hook(info: &PanicHookInfo<'_>) { use human_panic::{handle_dump, metadata, print_msg}; diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 2fdae1a..aa22e2b 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -14,7 +14,7 @@ use super::{ HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper, QueueEvent, Release, SecurityConfig, }, - HorizontallyScrollableText, Serdeable, + EnumDisplayStyle, HorizontallyScrollableText, Serdeable, }; #[cfg(test)] @@ -43,7 +43,7 @@ pub struct BlocklistResponse { pub records: Vec, } -#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DownloadRecord { pub title: String, @@ -52,16 +52,18 @@ pub struct DownloadRecord { pub id: i64, #[serde(deserialize_with = "super::from_i64")] pub episode_id: i64, - #[serde(deserialize_with = "super::from_i64")] - pub size: i64, - #[serde(deserialize_with = "super::from_i64")] - pub sizeleft: i64, + #[serde(deserialize_with = "super::from_f64")] + pub size: f64, + #[serde(deserialize_with = "super::from_f64")] + pub sizeleft: f64, pub output_path: Option, #[serde(default)] pub indexer: String, pub download_client: String, } +impl Eq for DownloadRecord {} + #[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DownloadsResponse { @@ -242,8 +244,8 @@ impl Display for SeriesType { } } -impl SeriesType { - pub fn to_display_str<'a>(self) -> &'a str { +impl<'a> EnumDisplayStyle<'a> for SeriesType { + fn to_display_str(self) -> &'a str { match self { SeriesType::Standard => "Standard", SeriesType::Daily => "Daily", @@ -293,8 +295,8 @@ impl Display for SeriesStatus { } } -impl SeriesStatus { - pub fn to_display_str<'a>(self) -> &'a str { +impl<'a> EnumDisplayStyle<'a> for SeriesStatus { + fn to_display_str(self) -> &'a str { match self { SeriesStatus::Continuing => "Continuing", SeriesStatus::Ended => "Ended", @@ -313,8 +315,62 @@ pub struct SonarrHistoryWrapper { #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct SonarrHistoryData { - pub dropped_path: String, - pub imported_path: String, + pub dropped_path: Option, + pub imported_path: Option, + pub indexer: Option, + pub release_group: Option, + pub series_match_type: Option, + pub nzb_info_url: Option, + pub download_client_name: Option, + pub age: Option, + pub published_date: Option>, + pub message: Option, + pub reason: Option, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum SonarrHistoryEventType { + #[default] + Unknown, + Grabbed, + SeriesFolderImported, + DownloadFolderImported, + DownloadFailed, + EpisodeFileDeleted, + EpisodeFileRenamed, + DownloadIgnored, +} + +impl Display for SonarrHistoryEventType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let event_type = match self { + SonarrHistoryEventType::Unknown => "unknown", + SonarrHistoryEventType::Grabbed => "grabbed", + SonarrHistoryEventType::SeriesFolderImported => "seriesFolderImported", + SonarrHistoryEventType::DownloadFolderImported => "downloadFolderImported", + SonarrHistoryEventType::DownloadFailed => "downloadFailed", + SonarrHistoryEventType::EpisodeFileDeleted => "episodeFileDeleted", + SonarrHistoryEventType::EpisodeFileRenamed => "episodeFileRenamed", + SonarrHistoryEventType::DownloadIgnored => "downloadIgnored", + }; + write!(f, "{event_type}") + } +} + +impl<'a> EnumDisplayStyle<'a> for SonarrHistoryEventType { + fn to_display_str(self) -> &'a str { + match self { + SonarrHistoryEventType::Unknown => "Unknown", + SonarrHistoryEventType::Grabbed => "Grabbed", + SonarrHistoryEventType::SeriesFolderImported => "Series Folder Imported", + SonarrHistoryEventType::DownloadFolderImported => "Download Folder Imported", + SonarrHistoryEventType::DownloadFailed => "Download Failed", + SonarrHistoryEventType::EpisodeFileDeleted => "Episode File Deleted", + SonarrHistoryEventType::EpisodeFileRenamed => "Episode File Renamed", + SonarrHistoryEventType::DownloadIgnored => "Download Ignored", + } + } } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] @@ -326,7 +382,7 @@ pub struct SonarrHistoryItem { #[serde(deserialize_with = "super::from_i64")] pub episode_id: i64, pub quality: QualityWrapper, - pub languages: Vec, + pub language: Language, pub date: DateTime, pub event_type: String, pub data: SonarrHistoryData, diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs index 81a2276..b65779f 100644 --- a/src/models/sonarr_models_tests.rs +++ b/src/models/sonarr_models_tests.rs @@ -9,10 +9,10 @@ mod tests { }, sonarr_models::{ BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, - IndexerSettings, Series, SeriesStatus, SeriesType, SonarrHistoryItem, SonarrSerdeable, - SystemStatus, + IndexerSettings, Series, SeriesStatus, SeriesType, SonarrHistoryEventType, SonarrHistoryItem, + SonarrSerdeable, SystemStatus, }, - Serdeable, + EnumDisplayStyle, Serdeable, }; #[test] @@ -56,6 +56,66 @@ mod tests { assert_str_eq!(SeriesType::Anime.to_display_str(), "Anime"); } + #[test] + fn test_sonarr_history_event_type_display() { + assert_str_eq!(SonarrHistoryEventType::Unknown.to_string(), "unknown",); + assert_str_eq!(SonarrHistoryEventType::Grabbed.to_string(), "grabbed",); + assert_str_eq!( + SonarrHistoryEventType::SeriesFolderImported.to_string(), + "seriesFolderImported", + ); + assert_str_eq!( + SonarrHistoryEventType::DownloadFolderImported.to_string(), + "downloadFolderImported", + ); + assert_str_eq!( + SonarrHistoryEventType::DownloadFailed.to_string(), + "downloadFailed", + ); + assert_str_eq!( + SonarrHistoryEventType::EpisodeFileDeleted.to_string(), + "episodeFileDeleted", + ); + assert_str_eq!( + SonarrHistoryEventType::EpisodeFileRenamed.to_string(), + "episodeFileRenamed", + ); + assert_str_eq!( + SonarrHistoryEventType::DownloadIgnored.to_string(), + "downloadIgnored", + ); + } + + #[test] + fn test_sonarr_history_event_type_to_display_str() { + assert_str_eq!(SonarrHistoryEventType::Unknown.to_display_str(), "Unknown",); + assert_str_eq!(SonarrHistoryEventType::Grabbed.to_display_str(), "Grabbed",); + assert_str_eq!( + SonarrHistoryEventType::SeriesFolderImported.to_display_str(), + "Series Folder Imported", + ); + assert_str_eq!( + SonarrHistoryEventType::DownloadFolderImported.to_display_str(), + "Download Folder Imported", + ); + assert_str_eq!( + SonarrHistoryEventType::DownloadFailed.to_display_str(), + "Download Failed", + ); + assert_str_eq!( + SonarrHistoryEventType::EpisodeFileDeleted.to_display_str(), + "Episode File Deleted", + ); + assert_str_eq!( + SonarrHistoryEventType::EpisodeFileRenamed.to_display_str(), + "Episode File Renamed", + ); + assert_str_eq!( + SonarrHistoryEventType::DownloadIgnored.to_display_str(), + "Download Ignored", + ); + } + #[test] fn test_sonarr_serdeable_from() { let sonarr_serdeable = SonarrSerdeable::Value(json!({})); diff --git a/src/utils.rs b/src/utils.rs index 816f0fb..b5e98db 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,13 +2,25 @@ use std::fs::{self, File}; use std::io::{BufRead, BufReader, Seek, SeekFrom}; use std::path::PathBuf; use std::process; +use std::sync::Arc; +use std::time::Duration; +use anyhow::anyhow; +use anyhow::Result; use colored::Colorize; -use log::LevelFilter; +use indicatif::{ProgressBar, ProgressStyle}; +use log::{error, LevelFilter}; use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; use regex::Regex; +use reqwest::{Certificate, Client}; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +use crate::app::{log_and_print_error, App, AppConfig}; +use crate::cli::{self, Command}; +use crate::network::Network; #[cfg(test)] #[path = "utils_tests.rs"] @@ -122,3 +134,126 @@ fn colorize_log_line(line: &str, re: &Regex) -> String { line.to_string() } } + +pub(super) fn load_config(path: &str) -> Result { + let file = File::open(path).map_err(|e| anyhow!(e))?; + let reader = BufReader::new(file); + let config = serde_yaml::from_reader(reader)?; + Ok(config) +} + +pub(super) fn build_network_client(config: &AppConfig) -> Client { + let mut client_builder = Client::builder() + .pool_max_idle_per_host(10) + .http2_keep_alive_interval(Duration::from_secs(5)) + .tcp_keepalive(Duration::from_secs(5)); + + if let Some(radarr_config) = &config.radarr { + if let Some(ref cert_path) = &radarr_config.ssl_cert_path { + let cert = create_cert(cert_path, "Radarr"); + client_builder = client_builder.add_root_certificate(cert); + } + } + + if let Some(sonarr_config) = &config.sonarr { + if let Some(ref cert_path) = &sonarr_config.ssl_cert_path { + let cert = create_cert(cert_path, "Sonarr"); + client_builder = client_builder.add_root_certificate(cert); + } + } + + match client_builder.build() { + Ok(client) => client, + Err(e) => { + error!("Unable to create reqwest client: {}", e); + eprintln!("error: {}", e.to_string().red()); + process::exit(1); + } + } +} + +pub(super) fn create_cert(cert_path: &String, servarr_name: &str) -> Certificate { + match fs::read(cert_path) { + Ok(cert) => match Certificate::from_pem(&cert) { + Ok(certificate) => certificate, + Err(_) => { + log_and_print_error(format!( + "Unable to read the specified {} SSL certificate", + servarr_name + )); + process::exit(1); + } + }, + Err(_) => { + log_and_print_error(format!( + "Unable to open specified {} SSL certificate", + servarr_name + )); + process::exit(1); + } + } +} + +pub(super) fn render_spinner() -> ProgressBar { + let pb = ProgressBar::new_spinner(); + pb.enable_steady_tick(Duration::from_millis(60)); + pb.set_style( + ProgressStyle::with_template("{spinner:.blue}") + .unwrap() + .tick_strings(&[ + "⢀⠀", "⡀⠀", "⠄⠀", "⢂⠀", "⡂⠀", "⠅⠀", "⢃⠀", "⡃⠀", "⠍⠀", "⢋⠀", "⡋⠀", "⠍⠁", "⢋⠁", "⡋⠁", "⠍⠉", + "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⢈⠩", "⡀⢙", "⠄⡙", "⢂⠩", "⡂⢘", "⠅⡘", "⢃⠨", "⡃⢐", + "⠍⡐", "⢋⠠", "⡋⢀", "⠍⡁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⠈⠩", + "⠀⢙", "⠀⡙", "⠀⠩", "⠀⢘", "⠀⡘", "⠀⠨", "⠀⢐", "⠀⡐", "⠀⠠", "⠀⢀", "⠀⡀", + ]), + ); + pb.set_message("Querying..."); + pb +} + +pub(super) async fn start_cli_with_spinner( + config: AppConfig, + reqwest_client: Client, + cancellation_token: CancellationToken, + app: Arc>>, + command: Command, +) { + config.verify_config_present_for_cli(&command); + app.lock().await.cli_mode = true; + let pb = render_spinner(); + let app_nw = Arc::clone(&app); + let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); + match cli::handle_command(&app, command, &mut network).await { + Ok(output) => { + pb.finish(); + println!("{}", output); + } + Err(e) => { + pb.finish(); + eprintln!("error: {}", e.to_string().red()); + process::exit(1); + } + } +} + +pub(super) async fn start_cli_no_spinner( + config: AppConfig, + reqwest_client: Client, + cancellation_token: CancellationToken, + app: Arc>>, + command: Command, +) { + config.verify_config_present_for_cli(&command); + app.lock().await.cli_mode = true; + let app_nw = Arc::clone(&app); + let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); + match cli::handle_command(&app, command, &mut network).await { + Ok(output) => { + println!("{}", output); + } + Err(e) => { + eprintln!("error: {}", e.to_string().red()); + process::exit(1); + } + } +}