feat(cli): Added a spinner to the CLI for long running commands like fetching releases

This commit is contained in:
2024-11-20 19:33:40 -07:00
parent f5631376af
commit 34157ef32f
19 changed files with 717 additions and 271 deletions
Generated
+55
View File
@@ -370,6 +370,19 @@ dependencies = [
"thiserror", "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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -565,6 +578,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -1117,6 +1136,19 @@ dependencies = [
"hashbrown", "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]] [[package]]
name = "indoc" name = "indoc"
version = "2.0.5" version = "2.0.5"
@@ -1289,6 +1321,7 @@ dependencies = [
"derivative", "derivative",
"dirs-next", "dirs-next",
"human-panic", "human-panic",
"indicatif",
"indoc", "indoc",
"itertools", "itertools",
"log", "log",
@@ -1458,6 +1491,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.5" version = "0.36.5"
@@ -1596,6 +1635,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "portable-atomic"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@@ -2675,6 +2720,16 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
+1
View File
@@ -46,6 +46,7 @@ colored = "2.1.0"
async-trait = "0.1.83" async-trait = "0.1.83"
dirs-next = "2.0.0" dirs-next = "2.0.0"
managarr-tree-widget = "0.24.0" managarr-tree-widget = "0.24.0"
indicatif = "0.17.9"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.16" assert_cmd = "2.0.16"
+3 -1
View File
@@ -237,8 +237,10 @@ tautulli:
Managarr supports using environment variables on startup so you don't have to always specify certain flags: Managarr supports using environment variables on startup so you don't have to always specify certain flags:
| Variable | Description | Equivalent Flag | | Variable | Description | Equivalent Flag |
| --------------------------------------- | -------------------------------- | -------------------------------- | |-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` | | `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!) ## 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) Progress for the beta release can be followed on my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr)
+6 -19
View File
@@ -42,15 +42,15 @@ pub enum Command {
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> { pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
fn with(app: &'a Arc<Mutex<App<'b>>>, command: T, network: &'a mut dyn NetworkTrait) -> Self; fn with(app: &'a Arc<Mutex<App<'b>>>, command: T, network: &'a mut dyn NetworkTrait) -> Self;
async fn handle(self) -> Result<()>; async fn handle(self) -> Result<String>;
} }
pub(crate) async fn handle_command( pub(crate) async fn handle_command(
app: &Arc<Mutex<App<'_>>>, app: &Arc<Mutex<App<'_>>>,
command: Command, command: Command,
network: &mut dyn NetworkTrait, network: &mut dyn NetworkTrait,
) -> Result<()> { ) -> Result<String> {
match command { let result = match command {
Command::Radarr(radarr_command) => { Command::Radarr(radarr_command) => {
RadarrCliHandler::with(app, radarr_command, network) RadarrCliHandler::with(app, radarr_command, network)
.handle() .handle()
@@ -61,10 +61,10 @@ pub(crate) async fn handle_command(
.handle() .handle()
.await? .await?
} }
_ => (), _ => String::new(),
} };
Ok(()) Ok(result)
} }
#[inline] #[inline]
@@ -88,16 +88,3 @@ pub fn mutex_flags_or_default(positive: bool, negative: bool, default_value: boo
default_value 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);
};
}
+19 -11
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
execute_network_event,
models::radarr_models::{AddMovieBody, AddOptions, MinimumAvailability, Monitor}, models::radarr_models::{AddMovieBody, AddOptions, MinimumAvailability, Monitor},
network::{radarr_network::RadarrEvent, NetworkTrait}, network::{radarr_network::RadarrEvent, NetworkTrait},
}; };
@@ -106,8 +105,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
RadarrAddCommand::Movie { RadarrAddCommand::Movie {
tmdb_id, tmdb_id,
root_folder_path, root_folder_path,
@@ -131,19 +130,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
search_for_movie: !no_search_for_movie, 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 } => { RadarrAddCommand::RootFolder { root_folder_path } => {
execute_network_event!( let resp = self
self, .network
RadarrEvent::AddRootFolder(Some(root_folder_path.clone())) .handle_network_event((RadarrEvent::AddRootFolder(Some(root_folder_path.clone()))).into())
); .await?;
serde_json::to_string_pretty(&resp)?
} }
RadarrAddCommand::Tag { name } => { 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)
} }
} }
+34 -14
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
execute_network_event,
models::radarr_models::DeleteMovieParams, models::radarr_models::DeleteMovieParams,
network::{radarr_network::RadarrEvent, NetworkTrait}, network::{radarr_network::RadarrEvent, NetworkTrait},
}; };
@@ -85,19 +84,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => { RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
execute_network_event!( let resp = self
self, .network
RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)) .handle_network_event((RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))).into())
); .await?;
serde_json::to_string_pretty(&resp)?
} }
RadarrDeleteCommand::Download { download_id } => { 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 } => { 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 { RadarrDeleteCommand::Movie {
movie_id, movie_id,
@@ -109,16 +117,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
delete_movie_files: delete_files_from_disk, delete_movie_files: delete_files_from_disk,
add_list_exclusion, 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 } => { 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 } => { 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)
} }
} }
+26 -25
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command}, cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command},
execute_network_event,
models::{ models::{
radarr_models::{ radarr_models::{
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings, EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
@@ -339,8 +338,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
RadarrEditCommand::AllIndexerSettings { RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs, allow_hardcoded_subs,
disable_allow_hardcoded_subs, disable_allow_hardcoded_subs,
@@ -389,11 +388,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
}) })
.into(), .into(),
}; };
execute_network_event!( self
self, .network
RadarrEvent::EditAllIndexerSettings(Some(params)), .handle_network_event((RadarrEvent::EditAllIndexerSettings(Some(params))).into())
"All indexer settings updated" .await?;
); "All indexer settings updated".to_owned()
} else {
String::new()
} }
} }
RadarrEditCommand::Collection { RadarrEditCommand::Collection {
@@ -417,11 +418,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
root_folder_path, root_folder_path,
search_on_add: search_on_add_value, search_on_add: search_on_add_value,
}; };
execute_network_event!( self
self, .network
RadarrEvent::EditCollection(Some(edit_collection_params)), .handle_network_event((RadarrEvent::EditCollection(Some(edit_collection_params))).into())
"Collection Updated" .await?;
); "Collection updated".to_owned()
} }
RadarrEditCommand::Indexer { RadarrEditCommand::Indexer {
indexer_id, indexer_id,
@@ -458,11 +459,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
clear_tags, clear_tags,
}; };
execute_network_event!( self
self, .network
RadarrEvent::EditIndexer(Some(edit_indexer_params)), .handle_network_event((RadarrEvent::EditIndexer(Some(edit_indexer_params))).into())
"Indexer updated" .await?;
); "Indexer updated".to_owned()
} }
RadarrEditCommand::Movie { RadarrEditCommand::Movie {
movie_id, movie_id,
@@ -485,14 +486,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
clear_tags, clear_tags,
}; };
execute_network_event!( self
self, .network
RadarrEvent::EditMovie(Some(edit_movie_params)), .handle_network_event((RadarrEvent::EditMovie(Some(edit_movie_params))).into())
"Movie updated" .await?;
); "Movie Updated".to_owned()
}
} }
};
Ok(()) Ok(result)
} }
} }
+34 -11
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait}, network::{radarr_network::RadarrEvent, NetworkTrait},
}; };
@@ -72,28 +71,52 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
RadarrGetCommand::AllIndexerSettings => { 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 => { 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 } => { 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 } => { 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 => { 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 => { 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)
} }
} }
+66 -21
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait}, network::{radarr_network::RadarrEvent, NetworkTrait},
}; };
@@ -87,19 +86,35 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
RadarrListCommand::Blocklist => { 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 => { 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 => { 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 => { 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 { RadarrListCommand::Logs {
events, events,
@@ -113,39 +128,69 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
if output_in_log_format { if output_in_log_format {
let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone(); let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone();
let json = serde_json::to_string_pretty(&log_lines)?; serde_json::to_string_pretty(&log_lines)?
println!("{}", json);
} else { } else {
let json = serde_json::to_string_pretty(&logs)?; serde_json::to_string_pretty(&logs)?
println!("{}", json);
} }
} }
RadarrListCommand::Movies => { 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 } => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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)
} }
} }
+44 -13
View File
@@ -12,7 +12,6 @@ use tokio::sync::Mutex;
use crate::app::App; use crate::app::App;
use crate::cli::CliCommandHandler; use crate::cli::CliCommandHandler;
use crate::execute_network_event;
use crate::models::radarr_models::{ReleaseDownloadBody, TaskName}; use crate::models::radarr_models::{ReleaseDownloadBody, TaskName};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkTrait; use crate::network::NetworkTrait;
@@ -155,8 +154,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
RadarrCommand::Add(add_command) => { RadarrCommand::Add(add_command) => {
RadarrAddCommandHandler::with(self.app, add_command, self.network) RadarrAddCommandHandler::with(self.app, add_command, self.network)
.handle() .handle()
@@ -192,7 +191,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
.network .network
.handle_network_event(RadarrEvent::GetBlocklist.into()) .handle_network_event(RadarrEvent::GetBlocklist.into())
.await?; .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 { RadarrCommand::DownloadRelease {
guid, guid,
@@ -204,29 +207,57 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
indexer_id, indexer_id,
movie_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 } => { RadarrCommand::ManualSearch { movie_id } => {
println!("Searching for releases. This may take a minute..."); 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 } => { 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 } => { 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 } => { 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 => { 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 } => { 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)
} }
} }
+24 -9
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait}, network::{radarr_network::RadarrEvent, NetworkTrait},
}; };
@@ -63,22 +62,38 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand>
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
RadarrRefreshCommand::AllMovies => { 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 => { 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 => { 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 } => { 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)
} }
} }
+9 -9
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
execute_network_event,
network::{sonarr_network::SonarrEvent, NetworkTrait}, network::{sonarr_network::SonarrEvent, NetworkTrait},
}; };
@@ -55,16 +54,17 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let resp = match self.command {
SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => { SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
execute_network_event!( let resp = self
self, .network
SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)) .handle_network_event((SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))).into())
); .await?;
} serde_json::to_string_pretty(&resp)?
} }
};
Ok(()) Ok(resp)
} }
} }
+34 -11
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
execute_network_event,
network::{sonarr_network::SonarrEvent, NetworkTrait}, network::{sonarr_network::SonarrEvent, NetworkTrait},
}; };
@@ -72,28 +71,52 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHan
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
SonarrGetCommand::AllIndexerSettings => { 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 } => { 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 => { 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 => { 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 } => { 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 => { 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)
} }
} }
+51 -18
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
execute_network_event,
network::{sonarr_network::SonarrEvent, NetworkTrait}, network::{sonarr_network::SonarrEvent, NetworkTrait},
}; };
@@ -91,22 +90,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
SonarrListCommand::Blocklist => { 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 => { 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 } => { 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 } => { 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 => { 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 { SonarrListCommand::Logs {
events, events,
@@ -120,27 +139,41 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
if output_in_log_format { if output_in_log_format {
let log_lines = self.app.lock().await.data.sonarr_data.logs.items.clone(); let log_lines = self.app.lock().await.data.sonarr_data.logs.items.clone();
let json = serde_json::to_string_pretty(&log_lines)?; serde_json::to_string_pretty(&log_lines)?
println!("{}", json);
} else { } else {
let json = serde_json::to_string_pretty(&logs)?; serde_json::to_string_pretty(&logs)?
println!("{}", json);
} }
} }
SonarrListCommand::QualityProfiles => { 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 => { 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 => { 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 } => { 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)
} }
} }
+21 -11
View File
@@ -9,7 +9,6 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
app::App, app::App,
execute_network_event,
network::{sonarr_network::SonarrEvent, NetworkTrait}, network::{sonarr_network::SonarrEvent, NetworkTrait},
}; };
@@ -91,8 +90,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
} }
} }
async fn handle(self) -> Result<()> { async fn handle(self) -> Result<String> {
match self.command { let result = match self.command {
SonarrCommand::Delete(delete_command) => { SonarrCommand::Delete(delete_command) => {
SonarrDeleteCommandHandler::with(self.app, delete_command, self.network) SonarrDeleteCommandHandler::with(self.app, delete_command, self.network)
.handle() .handle()
@@ -113,24 +112,35 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
.network .network
.handle_network_event(SonarrEvent::GetBlocklist.into()) .handle_network_event(SonarrEvent::GetBlocklist.into())
.await?; .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 } => { SonarrCommand::ManualEpisodeSearch { episode_id } => {
println!("Searching for episode releases. This may take a minute..."); 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 { SonarrCommand::ManualSeasonSearch {
series_id, series_id,
season_number, season_number,
} => { } => {
println!("Searching for season releases. This may take a minute..."); println!("Searching for season releases. This may take a minute...");
execute_network_event!( let resp = self
self, .network
SonarrEvent::GetSeasonReleases(Some((series_id, season_number))) .handle_network_event(
); SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(),
} )
.await?;
serde_json::to_string_pretty(&resp)?
} }
};
Ok(()) Ok(result)
} }
} }
+20 -79
View File
@@ -1,20 +1,13 @@
#![warn(rust_2018_idioms)] #![warn(rust_2018_idioms)]
use std::fs::{self, File}; use anyhow::Result;
use std::io::BufReader;
use std::panic::PanicHookInfo; use std::panic::PanicHookInfo;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use std::{io, panic, process}; use std::{io, panic, process};
use anyhow::anyhow; use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser};
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_complete::generate; use clap_complete::generate;
use colored::Colorize; use colored::Colorize;
use crossterm::execute; use crossterm::execute;
@@ -25,14 +18,17 @@ use log::{error, warn};
use network::NetworkTrait; use network::NetworkTrait;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::Terminal; use ratatui::Terminal;
use reqwest::{Certificate, Client}; use reqwest::Client;
use tokio::select; use tokio::select;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken; 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::cli::Command;
use crate::event::input_event::{Events, InputEvent}; use crate::event::input_event::{Events, InputEvent};
use crate::event::Key; use crate::event::Key;
@@ -67,6 +63,13 @@ mod utils;
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Option<Command>, command: Option<Command>,
#[arg(
long,
global = true,
env = "MANAGARR_DISABLE_SPINNER",
help = "Disable the spinner (can sometimes make parsing output challenging)"
)]
disable_spinner: bool,
#[arg( #[arg(
long, long,
global = true, global = true,
@@ -91,6 +94,7 @@ async fn main() -> Result<()> {
} else { } else {
confy::load("managarr", "config")? confy::load("managarr", "config")?
}; };
let spinner_disabled = args.disable_spinner;
config.validate(); config.validate();
let reqwest_client = build_network_client(&config); let reqwest_client = build_network_client(&config);
let (sync_network_tx, sync_network_rx) = mpsc::channel(500); let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
@@ -113,14 +117,10 @@ async fn main() -> Result<()> {
match args.command { match args.command {
Some(command) => match command { Some(command) => match command {
Command::Radarr(_) | Command::Sonarr(_) => { Command::Radarr(_) | Command::Sonarr(_) => {
config.verify_config_present_for_cli(&command); if spinner_disabled {
app.lock().await.cli_mode = true; start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
let app_nw = Arc::clone(&app); } else {
let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await;
if let Err(e) = cli::handle_command(&app, command, &mut network).await {
eprintln!("error: {}", e.to_string().red());
process::exit(1);
} }
} }
Command::Completions { shell } => { Command::Completions { shell } => {
@@ -237,65 +237,6 @@ fn panic_hook(info: &PanicHookInfo<'_>) {
.unwrap(); .unwrap();
} }
fn load_config(path: &str) -> Result<AppConfig> {
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))] #[cfg(not(debug_assertions))]
fn panic_hook(info: &PanicHookInfo<'_>) { fn panic_hook(info: &PanicHookInfo<'_>) {
use human_panic::{handle_dump, metadata, print_msg}; use human_panic::{handle_dump, metadata, print_msg};
+69 -13
View File
@@ -14,7 +14,7 @@ use super::{
HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper, QueueEvent, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper, QueueEvent,
Release, SecurityConfig, Release, SecurityConfig,
}, },
HorizontallyScrollableText, Serdeable, EnumDisplayStyle, HorizontallyScrollableText, Serdeable,
}; };
#[cfg(test)] #[cfg(test)]
@@ -43,7 +43,7 @@ pub struct BlocklistResponse {
pub records: Vec<BlocklistItem>, pub records: Vec<BlocklistItem>,
} }
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] #[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DownloadRecord { pub struct DownloadRecord {
pub title: String, pub title: String,
@@ -52,16 +52,18 @@ pub struct DownloadRecord {
pub id: i64, pub id: i64,
#[serde(deserialize_with = "super::from_i64")] #[serde(deserialize_with = "super::from_i64")]
pub episode_id: i64, pub episode_id: i64,
#[serde(deserialize_with = "super::from_i64")] #[serde(deserialize_with = "super::from_f64")]
pub size: i64, pub size: f64,
#[serde(deserialize_with = "super::from_i64")] #[serde(deserialize_with = "super::from_f64")]
pub sizeleft: i64, pub sizeleft: f64,
pub output_path: Option<HorizontallyScrollableText>, pub output_path: Option<HorizontallyScrollableText>,
#[serde(default)] #[serde(default)]
pub indexer: String, pub indexer: String,
pub download_client: String, pub download_client: String,
} }
impl Eq for DownloadRecord {}
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DownloadsResponse { pub struct DownloadsResponse {
@@ -242,8 +244,8 @@ impl Display for SeriesType {
} }
} }
impl SeriesType { impl<'a> EnumDisplayStyle<'a> for SeriesType {
pub fn to_display_str<'a>(self) -> &'a str { fn to_display_str(self) -> &'a str {
match self { match self {
SeriesType::Standard => "Standard", SeriesType::Standard => "Standard",
SeriesType::Daily => "Daily", SeriesType::Daily => "Daily",
@@ -293,8 +295,8 @@ impl Display for SeriesStatus {
} }
} }
impl SeriesStatus { impl<'a> EnumDisplayStyle<'a> for SeriesStatus {
pub fn to_display_str<'a>(self) -> &'a str { fn to_display_str(self) -> &'a str {
match self { match self {
SeriesStatus::Continuing => "Continuing", SeriesStatus::Continuing => "Continuing",
SeriesStatus::Ended => "Ended", SeriesStatus::Ended => "Ended",
@@ -313,8 +315,62 @@ pub struct SonarrHistoryWrapper {
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SonarrHistoryData { pub struct SonarrHistoryData {
pub dropped_path: String, pub dropped_path: Option<String>,
pub imported_path: String, pub imported_path: Option<String>,
pub indexer: Option<String>,
pub release_group: Option<String>,
pub series_match_type: Option<String>,
pub nzb_info_url: Option<String>,
pub download_client_name: Option<String>,
pub age: Option<String>,
pub published_date: Option<DateTime<Utc>>,
pub message: Option<String>,
pub reason: Option<String>,
}
#[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)] #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
@@ -326,7 +382,7 @@ pub struct SonarrHistoryItem {
#[serde(deserialize_with = "super::from_i64")] #[serde(deserialize_with = "super::from_i64")]
pub episode_id: i64, pub episode_id: i64,
pub quality: QualityWrapper, pub quality: QualityWrapper,
pub languages: Vec<Language>, pub language: Language,
pub date: DateTime<Utc>, pub date: DateTime<Utc>,
pub event_type: String, pub event_type: String,
pub data: SonarrHistoryData, pub data: SonarrHistoryData,
+63 -3
View File
@@ -9,10 +9,10 @@ mod tests {
}, },
sonarr_models::{ sonarr_models::{
BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, Episode,
IndexerSettings, Series, SeriesStatus, SeriesType, SonarrHistoryItem, SonarrSerdeable, IndexerSettings, Series, SeriesStatus, SeriesType, SonarrHistoryEventType, SonarrHistoryItem,
SystemStatus, SonarrSerdeable, SystemStatus,
}, },
Serdeable, EnumDisplayStyle, Serdeable,
}; };
#[test] #[test]
@@ -56,6 +56,66 @@ mod tests {
assert_str_eq!(SeriesType::Anime.to_display_str(), "Anime"); 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] #[test]
fn test_sonarr_serdeable_from() { fn test_sonarr_serdeable_from() {
let sonarr_serdeable = SonarrSerdeable::Value(json!({})); let sonarr_serdeable = SonarrSerdeable::Value(json!({}));
+136 -1
View File
@@ -2,13 +2,25 @@ use std::fs::{self, File};
use std::io::{BufRead, BufReader, Seek, SeekFrom}; use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::PathBuf; use std::path::PathBuf;
use std::process; use std::process;
use std::sync::Arc;
use std::time::Duration;
use anyhow::anyhow;
use anyhow::Result;
use colored::Colorize; use colored::Colorize;
use log::LevelFilter; use indicatif::{ProgressBar, ProgressStyle};
use log::{error, LevelFilter};
use log4rs::append::file::FileAppender; use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root}; use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder; use log4rs::encode::pattern::PatternEncoder;
use regex::Regex; 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)] #[cfg(test)]
#[path = "utils_tests.rs"] #[path = "utils_tests.rs"]
@@ -122,3 +134,126 @@ fn colorize_log_line(line: &str, re: &Regex) -> String {
line.to_string() line.to_string()
} }
} }
pub(super) fn load_config(path: &str) -> Result<AppConfig> {
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<Mutex<App<'_>>>,
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<Mutex<App<'_>>>,
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);
}
}
}