Added the full Radarr CLI so users can programmatically access all the same management features as in the TUI

This commit is contained in:
2024-10-29 18:47:40 -06:00
parent 217d3242a8
commit 1f8d72c939
65 changed files with 9401 additions and 1370 deletions
+12 -10
View File
@@ -49,14 +49,14 @@ impl<'a> App<'a> {
.dispatch_network_event(RadarrEvent::GetIndexers.into())
.await;
}
ActiveRadarrBlock::IndexerSettingsPrompt => {
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
self
.dispatch_network_event(RadarrEvent::GetIndexerSettings.into())
.dispatch_network_event(RadarrEvent::GetAllIndexerSettings.into())
.await;
}
ActiveRadarrBlock::TestIndexer => {
self
.dispatch_network_event(RadarrEvent::TestIndexer.into())
.dispatch_network_event(RadarrEvent::TestIndexer(None).into())
.await;
}
ActiveRadarrBlock::TestAllIndexers => {
@@ -72,7 +72,7 @@ impl<'a> App<'a> {
.dispatch_network_event(RadarrEvent::GetQueuedEvents.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetLogs.into())
.dispatch_network_event(RadarrEvent::GetLogs(None).into())
.await;
}
ActiveRadarrBlock::SystemUpdates => {
@@ -82,17 +82,17 @@ impl<'a> App<'a> {
}
ActiveRadarrBlock::AddMovieSearchResults => {
self
.dispatch_network_event(RadarrEvent::SearchNewMovie.into())
.dispatch_network_event(RadarrEvent::SearchNewMovie(None).into())
.await;
}
ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => {
self
.dispatch_network_event(RadarrEvent::GetMovieDetails.into())
.dispatch_network_event(RadarrEvent::GetMovieDetails(None).into())
.await;
}
ActiveRadarrBlock::MovieHistory => {
self
.dispatch_network_event(RadarrEvent::GetMovieHistory.into())
.dispatch_network_event(RadarrEvent::GetMovieHistory(None).into())
.await;
}
ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => {
@@ -102,7 +102,7 @@ impl<'a> App<'a> {
|| movie_details_modal.movie_crew.items.is_empty() =>
{
self
.dispatch_network_event(RadarrEvent::GetMovieCredits.into())
.dispatch_network_event(RadarrEvent::GetMovieCredits(None).into())
.await;
}
_ => (),
@@ -111,7 +111,7 @@ impl<'a> App<'a> {
ActiveRadarrBlock::ManualSearch => match self.data.radarr_data.movie_details_modal.as_ref() {
Some(movie_details_modal) if movie_details_modal.movie_releases.items.is_empty() => {
self
.dispatch_network_event(RadarrEvent::GetReleases.into())
.dispatch_network_event(RadarrEvent::GetReleases(None).into())
.await;
}
_ => (),
@@ -127,7 +127,9 @@ impl<'a> App<'a> {
if self.data.radarr_data.prompt_confirm {
self.data.radarr_data.prompt_confirm = false;
if let Some(radarr_event) = &self.data.radarr_data.prompt_confirm_action {
self.dispatch_network_event((*radarr_event).into()).await;
self
.dispatch_network_event(radarr_event.clone().into())
.await;
self.should_refresh = true;
self.data.radarr_data.prompt_confirm_action = None;
}
+15 -15
View File
@@ -68,7 +68,7 @@ mod tests {
#[tokio::test]
async fn test_dispatch_by_collection_details_block_with_add_movie() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie);
app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None));
app.data.radarr_data.collections.set_items(vec![Collection {
movies: Some(vec![CollectionMovie::default()]),
@@ -82,7 +82,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::AddMovie.into()
RadarrEvent::AddMovie(None).into()
);
assert!(!app.data.radarr_data.collection_movies.items.is_empty());
assert_eq!(app.tick_count, 0);
@@ -162,17 +162,17 @@ mod tests {
}
#[tokio::test]
async fn test_dispatch_by_indexer_settings_block() {
async fn test_dispatch_by_all_indexer_settings_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_radarr_block(&ActiveRadarrBlock::IndexerSettingsPrompt)
.dispatch_by_radarr_block(&ActiveRadarrBlock::AllIndexerSettingsPrompt)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetIndexerSettings.into()
RadarrEvent::GetAllIndexerSettings.into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -189,7 +189,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::TestIndexer.into()
RadarrEvent::TestIndexer(None).into()
);
assert_eq!(app.tick_count, 0);
}
@@ -229,7 +229,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetLogs.into()
RadarrEvent::GetLogs(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -263,7 +263,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::SearchNewMovie.into()
RadarrEvent::SearchNewMovie(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -280,7 +280,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovieDetails.into()
RadarrEvent::GetMovieDetails(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -297,7 +297,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovieDetails.into()
RadarrEvent::GetMovieDetails(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -314,7 +314,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovieHistory.into()
RadarrEvent::GetMovieHistory(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -331,7 +331,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovieCredits.into()
RadarrEvent::GetMovieCredits(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -354,7 +354,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovieCredits.into()
RadarrEvent::GetMovieCredits(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -377,7 +377,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovieCredits.into()
RadarrEvent::GetMovieCredits(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -418,7 +418,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetReleases.into()
RadarrEvent::GetReleases(None).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
+130
View File
@@ -0,0 +1,130 @@
#[cfg(test)]
mod tests {
use std::sync::Arc;
use clap::{error::ErrorKind, CommandFactory};
use mockall::predicate::eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand},
models::{
radarr_models::{BlocklistItem, BlocklistResponse, RadarrSerdeable},
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
Cli,
};
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_subcommand_requires_subcommand() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
}
#[test]
fn test_radarr_subcommand_delegates_to_radarr() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "all-indexer-settings"]);
assert!(result.is_ok());
}
#[test]
fn test_completions_requires_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
}
#[test]
fn test_completions_invalid_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions", "test"]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_completions_satisfied_with_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions", "bash"]);
assert!(result.is_ok());
}
#[rstest]
#[case(false, false, None)]
#[case(false, true, Some(false))]
#[case(true, false, Some(true))]
fn test_mutex_flags_or_option(
#[case] positive: bool,
#[case] negative: bool,
#[case] expected_output: Option<bool>,
) {
let result = mutex_flags_or_option(positive, negative);
assert_eq!(result, expected_output);
}
#[rstest]
#[case(false, false, true, true)]
#[case(false, false, false, false)]
#[case(false, true, true, false)]
#[case(true, false, false, true)]
fn test_mutex_flags_or_default(
#[case] positive: bool,
#[case] negative: bool,
#[case] default_value: bool,
#[case] expected_output: bool,
) {
use crate::cli::mutex_flags_or_default;
let result = mutex_flags_or_default(positive, negative, default_value);
assert_eq!(result, expected_output);
}
#[tokio::test]
async fn test_cli_handler_delegates_radarr_commands_to_the_radarr_cli_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(RadarrEvent::GetBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse(
BlocklistResponse {
records: vec![BlocklistItem::default()],
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(RadarrEvent::ClearBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let claer_blocklist_command = RadarrCommand::ClearBlocklist.into();
let result = handle_command(&app_arc, claer_blocklist_command, &mut mock_network).await;
assert!(result.is_ok());
}
}
+83
View File
@@ -0,0 +1,83 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{command, Subcommand};
use clap_complete::Shell;
use radarr::{RadarrCliHandler, RadarrCommand};
use tokio::sync::Mutex;
use crate::{app::App, network::NetworkTrait};
pub mod radarr;
#[cfg(test)]
#[path = "cli_tests.rs"]
mod cli_tests;
#[derive(Debug, Clone, Subcommand, PartialEq, Eq)]
pub enum Command {
#[command(subcommand, about = "Commands for manging your Radarr instance")]
Radarr(RadarrCommand),
#[command(
arg_required_else_help = true,
about = "Generate shell completions for the Managarr CLI"
)]
Completions {
#[arg(value_enum)]
shell: Shell,
},
}
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
fn with(app: &'a Arc<Mutex<App<'b>>>, command: T, network: &'a mut dyn NetworkTrait) -> Self;
async fn handle(self) -> Result<()>;
}
pub(crate) async fn handle_command(
app: &Arc<Mutex<App<'_>>>,
command: Command,
network: &mut dyn NetworkTrait,
) -> Result<()> {
if let Command::Radarr(radarr_command) = command {
RadarrCliHandler::with(app, radarr_command, network)
.handle()
.await?
}
Ok(())
}
#[inline]
pub fn mutex_flags_or_option(positive: bool, negative: bool) -> Option<bool> {
if positive {
Some(true)
} else if negative {
Some(false)
} else {
None
}
}
#[inline]
pub fn mutex_flags_or_default(positive: bool, negative: bool, default_value: bool) -> bool {
if positive {
true
} else if negative {
false
} else {
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);
};
}
+145
View File
@@ -0,0 +1,145 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{arg, command, ArgAction, Subcommand};
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},
};
use super::RadarrCommand;
#[cfg(test)]
#[path = "add_command_handler_tests.rs"]
mod add_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RadarrAddCommand {
#[command(about = "Add a new movie to your Radarr library")]
Movie {
#[arg(
long,
help = "The TMDB ID of the film you wish to add to your library",
required = true
)]
tmdb_id: i64,
#[arg(
long,
help = "The root folder path where all film data and metadata should live",
required = true
)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the quality profile to use for this movie",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The minimum availability to monitor for this film",
value_enum,
default_value_t = MinimumAvailability::default()
)]
minimum_availability: MinimumAvailability,
#[arg(long, help = "Should Radarr monitor this film")]
disable_monitoring: bool,
#[arg(
long,
help = "Tag IDs to tag the film with",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
#[arg(
long,
help = "What Radarr should monitor",
value_enum,
default_value_t = Monitor::default()
)]
monitor: Monitor,
#[arg(
long,
help = "Tell Radarr to not start a search for this film once it's added to your library",
)]
no_search_for_movie: bool,
},
#[command(about = "Add a new root folder")]
RootFolder {
#[arg(long, help = "The path of the new root folder", required = true)]
root_folder_path: String,
},
#[command(about = "Add new tag")]
Tag {
#[arg(long, help = "The name of the tag to be added", required = true)]
name: String
},
}
impl From<RadarrAddCommand> for Command {
fn from(value: RadarrAddCommand) -> Self {
Command::Radarr(RadarrCommand::Add(value))
}
}
pub(super) struct RadarrAddCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrAddCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHandler<'a, 'b> {
fn with(_app: &'a Arc<Mutex<App<'b>>>, command: RadarrAddCommand, network: &'a mut dyn NetworkTrait) -> Self {
RadarrAddCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<()> {
match self.command {
RadarrAddCommand::Movie {
tmdb_id,
root_folder_path,
quality_profile_id,
minimum_availability,
disable_monitoring,
tag: tags,
monitor,
no_search_for_movie,
} => {
let body = AddMovieBody {
tmdb_id,
title: String::new(),
root_folder_path,
quality_profile_id,
minimum_availability: minimum_availability.to_string(),
monitored: !disable_monitoring,
tags,
add_options: AddOptions {
monitor: monitor.to_string(),
search_for_movie: !no_search_for_movie,
},
};
execute_network_event!(self, RadarrEvent::AddMovie(Some(body)));
}
RadarrAddCommand::RootFolder { root_folder_path } => {
execute_network_event!(
self,
RadarrEvent::AddRootFolder(Some(root_folder_path.clone()))
);
}
RadarrAddCommand::Tag { name } => {
execute_network_event!(self, RadarrEvent::AddTag(name.clone()));
}
}
Ok(())
}
}
+472
View File
@@ -0,0 +1,472 @@
#[cfg(test)]
mod tests {
use clap::{error::ErrorKind, CommandFactory, Parser};
use crate::{
cli::{
radarr::{add_command_handler::RadarrAddCommand, RadarrCommand},
Command,
},
models::radarr_models::{MinimumAvailability, Monitor},
Cli,
};
#[test]
fn test_radarr_add_command_from() {
let command = RadarrAddCommand::Tag {
name: String::new(),
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Radarr(RadarrCommand::Add(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_add_movie_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "add", "movie"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_movie_requires_root_folder_path() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"add",
"movie",
"--tmdb-id",
"1",
"--quality-profile-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_movie_requires_quality_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"add",
"movie",
"--tmdb-id",
"1",
"--root-folder-path",
"/test",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_movie_requires_tmdb_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"add",
"movie",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_add_movie_assert_argument_flags_require_args(
#[values("--minimum-availability", "--tag", "--monitor")] flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"add",
"movie",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
flag,
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_movie_all_arguments_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"add",
"movie",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--tmdb-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_add_movie_minimum_availability_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"add",
"movie",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--tmdb-id",
"1",
"--minimum-availability",
"test",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_movie_monitor_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"add",
"movie",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--tmdb-id",
"1",
"--monitor",
"test",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_movie_defaults() {
let expected_args = RadarrAddCommand::Movie {
tmdb_id: 1,
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
minimum_availability: MinimumAvailability::default(),
disable_monitoring: false,
tag: vec![],
monitor: Monitor::default(),
no_search_for_movie: false,
};
let result = Cli::try_parse_from([
"managarr",
"radarr",
"add",
"movie",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--tmdb-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
#[test]
fn test_add_movie_tags_is_repeatable() {
let expected_args = RadarrAddCommand::Movie {
tmdb_id: 1,
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
minimum_availability: MinimumAvailability::default(),
disable_monitoring: false,
tag: vec![1, 2],
monitor: Monitor::default(),
no_search_for_movie: false,
};
let result = Cli::try_parse_from([
"managarr",
"radarr",
"add",
"movie",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--tmdb-id",
"1",
"--tag",
"1",
"--tag",
"2",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
#[test]
fn test_add_movie_all_args_defined() {
let expected_args = RadarrAddCommand::Movie {
tmdb_id: 1,
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
minimum_availability: MinimumAvailability::Released,
disable_monitoring: true,
tag: vec![1, 2],
monitor: Monitor::MovieAndCollection,
no_search_for_movie: true,
};
let result = Cli::try_parse_from([
"managarr",
"radarr",
"add",
"movie",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--minimum-availability",
"released",
"--disable-monitoring",
"--tmdb-id",
"1",
"--tag",
"1",
"--tag",
"2",
"--monitor",
"movie-and-collection",
"--no-search-for-movie",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
#[test]
fn test_add_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "add", "root-folder"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_root_folder_success() {
let expected_args = RadarrAddCommand::RootFolder {
root_folder_path: "/nfs/test".to_owned(),
};
let result = Cli::try_parse_from([
"managarr",
"radarr",
"add",
"root-folder",
"--root-folder-path",
"/nfs/test",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
#[test]
fn test_add_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "add", "tag"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_tag_success() {
let expected_args = RadarrAddCommand::Tag {
name: "test".to_owned(),
};
let result = Cli::try_parse_from(["managarr", "radarr", "add", "tag", "--name", "test"]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
}
mod handler {
use std::sync::Arc;
use crate::{
app::App,
cli::{radarr::add_command_handler::RadarrAddCommandHandler, CliCommandHandler},
models::{
radarr_models::{AddMovieBody, AddOptions, RadarrSerdeable},
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
};
use super::*;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_handle_add_movie_command() {
let expected_add_movie_body = AddMovieBody {
tmdb_id: 1,
title: String::new(),
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
minimum_availability: "released".to_owned(),
monitored: false,
tags: vec![1, 2],
add_options: AddOptions {
monitor: "movieAndCollection".to_owned(),
search_for_movie: false,
},
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::AddMovie(Some(expected_add_movie_body)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let add_movie_command = RadarrAddCommand::Movie {
tmdb_id: 1,
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
minimum_availability: MinimumAvailability::Released,
disable_monitoring: true,
tag: vec![1, 2],
monitor: Monitor::MovieAndCollection,
no_search_for_movie: true,
};
let result = RadarrAddCommandHandler::with(&app_arc, add_movie_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_add_root_folder_command() {
let expected_root_folder_path = "/nfs/test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let add_root_folder_command = RadarrAddCommand::RootFolder {
root_folder_path: expected_root_folder_path,
};
let result =
RadarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_add_tag_command() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let add_tag_command = RadarrAddCommand::Tag {
name: expected_tag_name,
};
let result = RadarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+124
View File
@@ -0,0 +1,124 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
models::radarr_models::DeleteMovieParams,
network::{radarr_network::RadarrEvent, NetworkTrait},
};
use super::RadarrCommand;
#[cfg(test)]
#[path = "delete_command_handler_tests.rs"]
mod delete_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RadarrDeleteCommand {
#[command(about = "Delete the specified item from the Radarr blocklist")]
BlocklistItem {
#[arg(
long,
help = "The ID of the blocklist item to remove from the blocklist",
required = true
)]
blocklist_item_id: i64,
},
#[command(about = "Delete the specified download")]
Download {
#[arg(long, help = "The ID of the download to delete", required = true)]
download_id: i64,
},
#[command(about = "Delete the indexer with the given ID")]
Indexer {
#[arg(long, help = "The ID of the indexer to delete", required = true)]
indexer_id: i64,
},
#[command(about = "Delete a movie from your Radarr library")]
Movie {
#[arg(long, help = "The ID of the movie to delete", required = true)]
movie_id: i64,
#[arg(long, help = "Delete the movie files from disk as well")]
delete_files_from_disk: bool,
#[arg(long, help = "Add a list exclusion for this film")]
add_list_exclusion: bool,
},
#[command(about = "Delete the root folder with the given ID")]
RootFolder {
#[arg(long, help = "The ID of the root folder to delete", required = true)]
root_folder_id: i64,
},
#[command(about = "Delete the tag with the specified ID")]
Tag {
#[arg(long, help = "The ID of the tag to delete", required = true)]
tag_id: i64,
},
}
impl From<RadarrDeleteCommand> for Command {
fn from(value: RadarrDeleteCommand) -> Self {
Command::Radarr(RadarrCommand::Delete(value))
}
}
pub(super) struct RadarrDeleteCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
RadarrDeleteCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<()> {
match self.command {
RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
execute_network_event!(
self,
RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))
);
}
RadarrDeleteCommand::Download { download_id } => {
execute_network_event!(self, RadarrEvent::DeleteDownload(Some(download_id)));
}
RadarrDeleteCommand::Indexer { indexer_id } => {
execute_network_event!(self, RadarrEvent::DeleteIndexer(Some(indexer_id)));
}
RadarrDeleteCommand::Movie {
movie_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_movie_params = DeleteMovieParams {
id: movie_id,
delete_movie_files: delete_files_from_disk,
add_list_exclusion,
};
execute_network_event!(self, RadarrEvent::DeleteMovie(Some(delete_movie_params)));
}
RadarrDeleteCommand::RootFolder { root_folder_id } => {
execute_network_event!(self, RadarrEvent::DeleteRootFolder(Some(root_folder_id)));
}
RadarrDeleteCommand::Tag { tag_id } => {
execute_network_event!(self, RadarrEvent::DeleteTag(tag_id));
}
}
Ok(())
}
}
@@ -0,0 +1,432 @@
#[cfg(test)]
mod tests {
use crate::{
cli::{
radarr::{delete_command_handler::RadarrDeleteCommand, RadarrCommand},
Command,
},
Cli,
};
use clap::{error::ErrorKind, CommandFactory, Parser};
#[test]
fn test_radarr_delete_command_from() {
let command = RadarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1,
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Radarr(RadarrCommand::Delete(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_delete_blocklist_item_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "blocklist-item"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_blocklist_item_success() {
let expected_args = RadarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1,
};
let result = Cli::try_parse_from([
"managarr",
"radarr",
"delete",
"blocklist-item",
"--blocklist-item-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_download_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "download"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_download_success() {
let expected_args = RadarrDeleteCommand::Download { download_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"radarr",
"delete",
"download",
"--download-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "indexer"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_indexer_success() {
let expected_args = RadarrDeleteCommand::Indexer { indexer_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"radarr",
"delete",
"indexer",
"--indexer-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_movie_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "movie"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_movie_defaults() {
let expected_args = RadarrDeleteCommand::Movie {
movie_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result =
Cli::try_parse_from(["managarr", "radarr", "delete", "movie", "--movie-id", "1"]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_movie_all_args_defined() {
let expected_args = RadarrDeleteCommand::Movie {
movie_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result = Cli::try_parse_from([
"managarr",
"radarr",
"delete",
"movie",
"--movie-id",
"1",
"--delete-files-from-disk",
"--add-list-exclusion",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "root-folder"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_root_folder_success() {
let expected_args = RadarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"radarr",
"delete",
"root-folder",
"--root-folder-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "tag"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_tag_success() {
let expected_args = RadarrDeleteCommand::Tag { tag_id: 1 };
let result = Cli::try_parse_from(["managarr", "radarr", "delete", "tag", "--tag-id", "1"]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
radarr::delete_command_handler::{RadarrDeleteCommand, RadarrDeleteCommandHandler},
CliCommandHandler,
},
models::{
radarr_models::{DeleteMovieParams, RadarrSerdeable},
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_delete_blocklist_item_command() {
let expected_blocklist_item_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_blocklist_item_command = RadarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1,
};
let result = RadarrDeleteCommandHandler::with(
&app_arc,
delete_blocklist_item_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_download_command() {
let expected_download_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::DeleteDownload(Some(expected_download_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_download_command = RadarrDeleteCommand::Download { download_id: 1 };
let result =
RadarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_indexer_command() {
let expected_indexer_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::DeleteIndexer(Some(expected_indexer_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_indexer_command = RadarrDeleteCommand::Indexer { indexer_id: 1 };
let result =
RadarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_movie_command() {
let expected_delete_movie_params = DeleteMovieParams {
id: 1,
delete_movie_files: true,
add_list_exclusion: true,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::DeleteMovie(Some(expected_delete_movie_params)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_movie_command = RadarrDeleteCommand::Movie {
movie_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result =
RadarrDeleteCommandHandler::with(&app_arc, delete_movie_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_root_folder_command() {
let expected_root_folder_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::DeleteRootFolder(Some(expected_root_folder_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_root_folder_command = RadarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result =
RadarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_tag_command() {
let expected_tag_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::DeleteTag(expected_tag_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_tag_command = RadarrDeleteCommand::Tag { tag_id: 1 };
let result =
RadarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+498
View File
@@ -0,0 +1,498 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, ArgGroup, Subcommand};
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,
MinimumAvailability, RadarrSerdeable,
},
Serdeable,
},
network::{radarr_network::RadarrEvent, NetworkTrait},
};
use super::RadarrCommand;
#[cfg(test)]
#[path = "edit_command_handler_tests.rs"]
mod edit_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RadarrEditCommand {
#[command(
about = "Edit and indexer settings that apply to all indexers",
group(
ArgGroup::new("edit_settings")
.args([
"allow_hardcoded_subs",
"disable_allow_hardcoded_subs",
"availability_delay",
"maximum_size",
"minimum_age",
"prefer_indexer_flags",
"disable_prefer_indexer_flags",
"retention",
"rss_sync_interval",
"whitelisted_subtitle_tags"
]).required(true)
.multiple(true))
)]
AllIndexerSettings {
#[arg(
long,
help = "Detected hardcoded subs will be automatically downloaded",
conflicts_with = "disable_allow_hardcoded_subs"
)]
allow_hardcoded_subs: bool,
#[arg(
long,
help = "Disable allowing detected hardcoded subs from being automatically downloaded",
conflicts_with = "allow_hardcoded_subs"
)]
disable_allow_hardcoded_subs: bool,
#[arg(
long,
help = "Amount of time in days before or after available date to search for Movie"
)]
availability_delay: Option<i64>,
#[arg(
long,
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
)]
maximum_size: Option<i64>,
#[arg(
long,
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
)]
minimum_age: Option<i64>,
#[arg(
long,
help = "Prioritize releases with special flags",
conflicts_with = "disable_prefer_indexer_flags"
)]
prefer_indexer_flags: bool,
#[arg(
long,
help = "Disable prioritizing releases with special flags",
conflicts_with = "prefer_indexer_flags"
)]
disable_prefer_indexer_flags: bool,
#[arg(
long,
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
)]
retention: Option<i64>,
#[arg(
long,
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
)]
rss_sync_interval: Option<i64>,
#[arg(
long,
help = "A comma separated list of subtitle tags that will not be considered hardcoded"
)]
whitelisted_subtitle_tags: Option<String>,
},
#[command(
about = "Edit preferences for the specified collection",
group(
ArgGroup::new("edit_collection")
.args([
"enable_monitoring",
"disable_monitoring",
"minimum_availability",
"quality_profile_id",
"root_folder_path",
"search_on_add",
"disable_search_on_add"
]).required(true)
.multiple(true))
)]
Collection {
#[arg(
long,
help = "The ID of the collection whose preferences you want to edit",
required = true
)]
collection_id: i64,
#[arg(
long,
help = "Monitor to automatically have movies from this collection added to your library",
conflicts_with = "disable_monitoring"
)]
enable_monitoring: bool,
#[arg(
long,
help = "Disable monitoring for this collection so movies from this collection are not automatically added to your library",
conflicts_with = "enable_monitoring"
)]
disable_monitoring: bool,
#[arg(
long,
help = "Specify the minimum availability for all movies in this collection",
value_enum
)]
minimum_availability: Option<MinimumAvailability>,
#[arg(
long,
help = "The ID of the quality profile that all movies in this collection should use"
)]
quality_profile_id: Option<i64>,
#[arg(
long,
help = "The root folder path that all movies in this collection should exist under"
)]
root_folder_path: Option<String>,
#[arg(
long,
help = "Search for movies from this collection when added to your library",
conflicts_with = "disable_search_on_add"
)]
search_on_add: bool,
#[arg(
long,
help = "Disable triggering searching whenever new movies are added to this collection",
conflicts_with = "search_on_add"
)]
disable_search_on_add: bool,
},
#[command(
about = "Edit preferences for the specified indexer",
group(
ArgGroup::new("edit_indexer")
.args([
"name",
"enable_rss",
"disable_rss",
"enable_automatic_search",
"disable_automatic_search",
"enable_interactive_search",
"disable_automatic_search",
"url",
"api_key",
"seed_ratio",
"tag",
"priority",
"clear_tags"
]).required(true)
.multiple(true))
)]
Indexer {
#[arg(
long,
help = "The ID of the indexer whose settings you wish to edit",
required = true
)]
indexer_id: i64,
#[arg(long, help = "The name of the indexer")]
name: Option<String>,
#[arg(
long,
help = "Indicate to Radarr that this indexer should be used when Radarr periodically looks for releases via RSS Sync",
conflicts_with = "disable_rss"
)]
enable_rss: bool,
#[arg(
long,
help = "Disable using this indexer when Radarr periodically looks for releases via RSS Sync",
conflicts_with = "enable_rss"
)]
disable_rss: bool,
#[arg(
long,
help = "Indicate to Radarr that this indexer should be used when automatic searches are performed via the UI or by Radarr",
conflicts_with = "disable_automatic_search"
)]
enable_automatic_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Radarr",
conflicts_with = "enable_automatic_search"
)]
disable_automatic_search: bool,
#[arg(
long,
help = "Indicate to Radarr that this indexer should be used when an interactive search is used",
conflicts_with = "disable_interactive_search"
)]
enable_interactive_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever an interactive search is performed",
conflicts_with = "enable_interactive_search"
)]
disable_interactive_search: bool,
#[arg(long, help = "The URL of the indexer")]
url: Option<String>,
#[arg(long, help = "The API key used to access the indexer's API")]
api_key: Option<String>,
#[arg(
long,
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
)]
seed_ratio: Option<String>,
#[arg(
long,
help = "Only use this indexer for movies with at least one matching tag ID. Leave blank to use with all movies.",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(
long,
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Radarr will still use all enabled indexers for RSS Sync and Searching"
)]
priority: Option<i64>,
#[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")]
clear_tags: bool,
},
#[command(
about = "Edit preferences for the specified movie",
group(
ArgGroup::new("edit_movie")
.args([
"enable_monitoring",
"disable_monitoring",
"minimum_availability",
"quality_profile_id",
"root_folder_path",
"tag",
"clear_tags"
]).required(true)
.multiple(true))
)]
Movie {
#[arg(
long,
help = "The ID of the movie whose settings you want to edit",
required = true
)]
movie_id: i64,
#[arg(
long,
help = "Enable monitoring of this movie in Radarr so Radarr will automatically download this movie if it is available",
conflicts_with = "disable_monitoring"
)]
enable_monitoring: bool,
#[arg(
long,
help = "Disable monitoring of this movie so Radarr does not automatically download the movie if it is found to be available",
conflicts_with = "enable_monitoring"
)]
disable_monitoring: bool,
#[arg(
long,
help = "The minimum availability to monitor for this film",
value_enum
)]
minimum_availability: Option<MinimumAvailability>,
#[arg(long, help = "The ID of the quality profile to use for this movie")]
quality_profile_id: Option<i64>,
#[arg(
long,
help = "The root folder path where all film data and metadata should live"
)]
root_folder_path: Option<String>,
#[arg(
long,
help = "Tag IDs to tag this movie with",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(long, help = "Clear all tags on this movie", conflicts_with = "tag")]
clear_tags: bool,
},
}
impl From<RadarrEditCommand> for Command {
fn from(value: RadarrEditCommand) -> Self {
Command::Radarr(RadarrCommand::Edit(value))
}
}
pub(super) struct RadarrEditCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrEditCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrEditCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
RadarrEditCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<()> {
match self.command {
RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs,
disable_allow_hardcoded_subs,
availability_delay,
maximum_size,
minimum_age,
prefer_indexer_flags,
disable_prefer_indexer_flags,
retention,
rss_sync_interval,
whitelisted_subtitle_tags,
} => {
if let Serdeable::Radarr(RadarrSerdeable::IndexerSettings(previous_indexer_settings)) = self
.network
.handle_network_event(RadarrEvent::GetAllIndexerSettings.into())
.await?
{
let allow_hardcoded_subs_value = mutex_flags_or_default(
allow_hardcoded_subs,
disable_allow_hardcoded_subs,
previous_indexer_settings.allow_hardcoded_subs,
);
let prefer_indexer_flags_value = mutex_flags_or_default(
prefer_indexer_flags,
disable_prefer_indexer_flags,
previous_indexer_settings.prefer_indexer_flags,
);
let params = IndexerSettings {
id: 1,
allow_hardcoded_subs: allow_hardcoded_subs_value,
availability_delay: availability_delay
.unwrap_or(previous_indexer_settings.availability_delay),
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
prefer_indexer_flags: prefer_indexer_flags_value,
retention: retention.unwrap_or(previous_indexer_settings.retention),
rss_sync_interval: rss_sync_interval
.unwrap_or(previous_indexer_settings.rss_sync_interval),
whitelisted_hardcoded_subs: whitelisted_subtitle_tags
.clone()
.unwrap_or_else(|| {
previous_indexer_settings
.whitelisted_hardcoded_subs
.text
.clone()
})
.into(),
};
execute_network_event!(
self,
RadarrEvent::EditAllIndexerSettings(Some(params)),
"All indexer settings updated"
);
}
}
RadarrEditCommand::Collection {
collection_id,
enable_monitoring,
disable_monitoring,
minimum_availability,
quality_profile_id,
root_folder_path,
search_on_add,
disable_search_on_add,
} => {
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
let search_on_add_value = mutex_flags_or_option(search_on_add, disable_search_on_add);
let edit_collection_params = EditCollectionParams {
collection_id,
monitored: monitored_value,
minimum_availability,
quality_profile_id,
root_folder_path,
search_on_add: search_on_add_value,
};
execute_network_event!(
self,
RadarrEvent::EditCollection(Some(edit_collection_params)),
"Collection Updated"
);
}
RadarrEditCommand::Indexer {
indexer_id,
name,
enable_rss,
disable_rss,
enable_automatic_search,
disable_automatic_search,
enable_interactive_search,
disable_interactive_search,
url,
api_key,
seed_ratio,
tag,
priority,
clear_tags,
} => {
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
let automatic_search_value =
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
let interactive_search_value =
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
let edit_indexer_params = EditIndexerParams {
indexer_id,
name,
enable_rss: rss_value,
enable_automatic_search: automatic_search_value,
enable_interactive_search: interactive_search_value,
url,
api_key,
seed_ratio,
tags: tag,
priority,
clear_tags,
};
execute_network_event!(
self,
RadarrEvent::EditIndexer(Some(edit_indexer_params)),
"Indexer updated"
);
}
RadarrEditCommand::Movie {
movie_id,
enable_monitoring,
disable_monitoring,
minimum_availability,
quality_profile_id,
root_folder_path,
tag,
clear_tags,
} => {
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
let edit_movie_params = EditMovieParams {
movie_id,
monitored: monitored_value,
minimum_availability,
quality_profile_id,
root_folder_path,
tags: tag,
clear_tags,
};
execute_network_event!(
self,
RadarrEvent::EditMovie(Some(edit_movie_params)),
"Movie updated"
);
}
}
Ok(())
}
}
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{command, Subcommand};
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait},
};
use super::RadarrCommand;
#[cfg(test)]
#[path = "get_command_handler_tests.rs"]
mod get_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RadarrGetCommand {
#[command(about = "Get the shared settings for all indexers")]
AllIndexerSettings,
#[command(about = "Get detailed information for the movie with the given ID")]
MovieDetails {
#[arg(
long,
help = "The Radarr ID of the movie whose details you wish to fetch",
required = true
)]
movie_id: i64,
},
#[command(about = "Get history for the given movie ID")]
MovieHistory {
#[arg(
long,
help = "The Radarr ID of the movie whose history you wish to fetch",
required = true
)]
movie_id: i64,
},
#[command(about = "Get the system status")]
SystemStatus,
}
impl From<RadarrGetCommand> for Command {
fn from(value: RadarrGetCommand) -> Self {
Command::Radarr(RadarrCommand::Get(value))
}
}
pub(super) struct RadarrGetCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrGetCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrGetCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
RadarrGetCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<()> {
match self.command {
RadarrGetCommand::AllIndexerSettings => {
execute_network_event!(self, RadarrEvent::GetAllIndexerSettings);
}
RadarrGetCommand::MovieDetails { movie_id } => {
execute_network_event!(self, RadarrEvent::GetMovieDetails(Some(movie_id)));
}
RadarrGetCommand::MovieHistory { movie_id } => {
execute_network_event!(self, RadarrEvent::GetMovieHistory(Some(movie_id)));
}
RadarrGetCommand::SystemStatus => {
execute_network_event!(self, RadarrEvent::GetStatus);
}
}
Ok(())
}
}
+213
View File
@@ -0,0 +1,213 @@
#[cfg(test)]
mod test {
use clap::error::ErrorKind;
use clap::CommandFactory;
use crate::cli::radarr::get_command_handler::RadarrGetCommand;
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
#[test]
fn test_radarr_get_command_from() {
let command = RadarrGetCommand::AllIndexerSettings;
let result = Command::from(command.clone());
assert_eq!(result, Command::Radarr(RadarrCommand::Get(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_all_indexer_settings_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "all-indexer-settings"]);
assert!(result.is_ok());
}
#[test]
fn test_movie_details_requires_movie_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "movie-details"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_movie_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"get",
"movie-details",
"--movie-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_movie_history_requires_movie_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "movie-history"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_movie_history_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"get",
"movie-history",
"--movie-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_system_status_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "system-status"]);
assert!(result.is_ok());
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
radarr::get_command_handler::{RadarrGetCommand, RadarrGetCommandHandler},
CliCommandHandler,
},
models::{radarr_models::RadarrSerdeable, Serdeable},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_get_all_indexer_settings_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_all_indexer_settings_command = RadarrGetCommand::AllIndexerSettings;
let result = RadarrGetCommandHandler::with(
&app_arc,
get_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_movie_details_command() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetMovieDetails(Some(expected_movie_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_movie_details_command = RadarrGetCommand::MovieDetails { movie_id: 1 };
let result =
RadarrGetCommandHandler::with(&app_arc, get_movie_details_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_movie_history_command() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetMovieHistory(Some(expected_movie_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_movie_history_command = RadarrGetCommand::MovieHistory { movie_id: 1 };
let result =
RadarrGetCommandHandler::with(&app_arc, get_movie_history_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_system_status_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(RadarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_system_status_command = RadarrGetCommand::SystemStatus;
let result =
RadarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+151
View File
@@ -0,0 +1,151 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{command, Subcommand};
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait},
};
use super::RadarrCommand;
#[cfg(test)]
#[path = "list_command_handler_tests.rs"]
mod list_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RadarrListCommand {
#[command(about = "List all items in the Radarr blocklist")]
Blocklist,
#[command(about = "List all Radarr collections")]
Collections,
#[command(about = "List all active downloads in Radarr")]
Downloads,
#[command(about = "List all Radarr indexers")]
Indexers,
#[command(about = "Fetch Radarr logs")]
Logs {
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
events: u64,
#[arg(
long,
help = "Output the logs in the same format as they appear in the log files"
)]
output_in_log_format: bool,
},
#[command(about = "List all movies in your Radarr library")]
Movies,
#[command(about = "Get the credits for the movie with the given ID")]
MovieCredits {
#[arg(
long,
help = "The Radarr ID of the movie whose credits you wish to fetch",
required = true
)]
movie_id: i64,
},
#[command(about = "List all Radarr quality profiles")]
QualityProfiles,
#[command(about = "List all queued events")]
QueuedEvents,
#[command(about = "List all root folders in Radarr")]
RootFolders,
#[command(about = "List all Radarr tags")]
Tags,
#[command(about = "List tasks")]
Tasks,
#[command(about = "List all Radarr updates")]
Updates,
}
impl From<RadarrListCommand> for Command {
fn from(value: RadarrListCommand) -> Self {
Command::Radarr(RadarrCommand::List(value))
}
}
pub(super) struct RadarrListCommandHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: RadarrListCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: RadarrListCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
RadarrListCommandHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<()> {
match self.command {
RadarrListCommand::Blocklist => {
execute_network_event!(self, RadarrEvent::GetBlocklist);
}
RadarrListCommand::Collections => {
execute_network_event!(self, RadarrEvent::GetCollections);
}
RadarrListCommand::Downloads => {
execute_network_event!(self, RadarrEvent::GetDownloads);
}
RadarrListCommand::Indexers => {
execute_network_event!(self, RadarrEvent::GetIndexers);
}
RadarrListCommand::Logs {
events,
output_in_log_format,
} => {
let logs = self
.network
.handle_network_event(RadarrEvent::GetLogs(Some(events)).into())
.await?;
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);
} else {
let json = serde_json::to_string_pretty(&logs)?;
println!("{}", json);
}
}
RadarrListCommand::Movies => {
execute_network_event!(self, RadarrEvent::GetMovies);
}
RadarrListCommand::MovieCredits { movie_id } => {
execute_network_event!(self, RadarrEvent::GetMovieCredits(Some(movie_id)));
}
RadarrListCommand::QualityProfiles => {
execute_network_event!(self, RadarrEvent::GetQualityProfiles);
}
RadarrListCommand::QueuedEvents => {
execute_network_event!(self, RadarrEvent::GetQueuedEvents);
}
RadarrListCommand::RootFolders => {
execute_network_event!(self, RadarrEvent::GetRootFolders);
}
RadarrListCommand::Tags => {
execute_network_event!(self, RadarrEvent::GetTags);
}
RadarrListCommand::Tasks => {
execute_network_event!(self, RadarrEvent::GetTasks);
}
RadarrListCommand::Updates => {
execute_network_event!(self, RadarrEvent::GetUpdates);
}
}
Ok(())
}
}
@@ -0,0 +1,210 @@
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use clap::CommandFactory;
use crate::cli::radarr::list_command_handler::RadarrListCommand;
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
#[test]
fn test_radarr_list_command_from() {
let command = RadarrListCommand::Movies;
let result = Command::from(command.clone());
assert_eq!(result, Command::Radarr(RadarrCommand::List(command)));
}
mod cli {
use super::*;
use clap::Parser;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_list_commands_have_no_arg_requirements(
#[values(
"blocklist",
"collections",
"downloads",
"indexers",
"movies",
"quality-profiles",
"queued-events",
"root-folders",
"tags",
"tasks",
"updates"
)]
subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "list", subcommand]);
assert!(result.is_ok());
}
#[test]
fn test_list_movie_credits_requires_movie_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "movie-credits"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_logs_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "logs", "--events"]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_movie_credits_success() {
let expected_args = RadarrListCommand::MovieCredits { movie_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"radarr",
"list",
"movie-credits",
"--movie-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command {
assert_eq!(refresh_command, expected_args);
}
}
#[test]
fn test_list_logs_default_values() {
let expected_args = RadarrListCommand::Logs {
events: 500,
output_in_log_format: false,
};
let result = Cli::try_parse_from(["managarr", "radarr", "list", "logs"]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command {
assert_eq!(refresh_command, expected_args);
}
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::CliCommandHandler;
use crate::{
app::App,
cli::radarr::list_command_handler::{RadarrListCommand, RadarrListCommandHandler},
models::{radarr_models::RadarrSerdeable, Serdeable},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
};
#[rstest]
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
#[case(RadarrListCommand::QualityProfiles, RadarrEvent::GetQualityProfiles)]
#[case(RadarrListCommand::QueuedEvents, RadarrEvent::GetQueuedEvents)]
#[case(RadarrListCommand::RootFolders, RadarrEvent::GetRootFolders)]
#[case(RadarrListCommand::Tags, RadarrEvent::GetTags)]
#[case(RadarrListCommand::Tasks, RadarrEvent::GetTasks)]
#[case(RadarrListCommand::Updates, RadarrEvent::GetUpdates)]
#[tokio::test]
async fn test_handle_list_blocklist_command(
#[case] list_command: RadarrListCommand,
#[case] expected_radarr_event: RadarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(expected_radarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let result = RadarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_list_movie_credits_command() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetMovieCredits(Some(expected_movie_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_movie_credits_command = RadarrListCommand::MovieCredits { movie_id: 1 };
let result =
RadarrListCommandHandler::with(&app_arc, list_movie_credits_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_list_logs_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetLogs(Some(expected_events)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_logs_command = RadarrListCommand::Logs {
events: 1000,
output_in_log_format: false,
};
let result = RadarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+232
View File
@@ -0,0 +1,232 @@
use std::sync::Arc;
use add_command_handler::{RadarrAddCommand, RadarrAddCommandHandler};
use clap::Subcommand;
use delete_command_handler::{RadarrDeleteCommand, RadarrDeleteCommandHandler};
use edit_command_handler::{RadarrEditCommand, RadarrEditCommandHandler};
use get_command_handler::{RadarrGetCommand, RadarrGetCommandHandler};
use list_command_handler::{RadarrListCommand, RadarrListCommandHandler};
use refresh_command_handler::{RadarrRefreshCommand, RadarrRefreshCommandHandler};
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;
use anyhow::Result;
use super::Command;
mod add_command_handler;
mod delete_command_handler;
mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod refresh_command_handler;
#[cfg(test)]
#[path = "radarr_command_tests.rs"]
mod radarr_command_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RadarrCommand {
#[command(
subcommand,
about = "Commands to add or create new resources within your Radarr instance"
)]
Add(RadarrAddCommand),
#[command(
subcommand,
about = "Commands to delete resources from your Radarr instance"
)]
Delete(RadarrDeleteCommand),
#[command(
subcommand,
about = "Commands to edit resources in your Radarr instance"
)]
Edit(RadarrEditCommand),
#[command(
subcommand,
about = "Commands to fetch details of the resources in your Radarr instance"
)]
Get(RadarrGetCommand),
#[command(
subcommand,
about = "Commands to list attributes from your Radarr instance"
)]
List(RadarrListCommand),
#[command(
subcommand,
about = "Commands to refresh the data in your Radarr instance"
)]
Refresh(RadarrRefreshCommand),
#[command(about = "Clear the blocklist")]
ClearBlocklist,
#[command(about = "Manually download the given release for the specified movie ID")]
DownloadRelease {
#[arg(long, help = "The GUID of the release to download", required = true)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
#[arg(
long,
help = "The movie ID that the release is associated with",
required = true
)]
movie_id: i64,
},
#[command(about = "Trigger a manual search of releases for the movie with the given ID")]
ManualSearch {
#[arg(
long,
help = "The Radarr ID of the movie whose releases you wish to fetch and list",
required = true
)]
movie_id: i64,
},
#[command(about = "Search for a new film to add to Radarr")]
SearchNewMovie {
#[arg(
long,
help = "The title of the film you want to search for",
required = true
)]
query: String,
},
#[command(about = "Start the specified Radarr task")]
StartTask {
#[arg(
long,
help = "The name of the task to trigger",
value_enum,
required = true
)]
task_name: TaskName,
},
#[command(
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
)]
TestIndexer {
#[arg(long, help = "The ID of the indexer to test", required = true)]
indexer_id: i64,
},
#[command(about = "Test all indexers")]
TestAllIndexers,
#[command(about = "Trigger an automatic search for the movie with the specified ID")]
TriggerAutomaticSearch {
#[arg(
long,
help = "The ID of the movie you want to trigger an automatic search for",
required = true
)]
movie_id: i64,
},
}
impl From<RadarrCommand> for Command {
fn from(radarr_command: RadarrCommand) -> Command {
Command::Radarr(radarr_command)
}
}
pub(super) struct RadarrCliHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: RadarrCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: RadarrCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
RadarrCliHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<()> {
match self.command {
RadarrCommand::Add(add_command) => {
RadarrAddCommandHandler::with(self.app, add_command, self.network)
.handle()
.await?
}
RadarrCommand::Delete(delete_command) => {
RadarrDeleteCommandHandler::with(self.app, delete_command, self.network)
.handle()
.await?
}
RadarrCommand::Edit(edit_command) => {
RadarrEditCommandHandler::with(self.app, edit_command, self.network)
.handle()
.await?
}
RadarrCommand::Get(get_command) => {
RadarrGetCommandHandler::with(self.app, get_command, self.network)
.handle()
.await?
}
RadarrCommand::List(list_command) => {
RadarrListCommandHandler::with(self.app, list_command, self.network)
.handle()
.await?
}
RadarrCommand::Refresh(update_command) => {
RadarrRefreshCommandHandler::with(self.app, update_command, self.network)
.handle()
.await?
}
RadarrCommand::ClearBlocklist => {
self
.network
.handle_network_event(RadarrEvent::GetBlocklist.into())
.await?;
execute_network_event!(self, RadarrEvent::ClearBlocklist);
}
RadarrCommand::DownloadRelease {
guid,
indexer_id,
movie_id,
} => {
let params = ReleaseDownloadBody {
guid,
indexer_id,
movie_id,
};
execute_network_event!(self, RadarrEvent::DownloadRelease(Some(params)));
}
RadarrCommand::ManualSearch { movie_id } => {
println!("Searching for releases. This may take a minute...");
execute_network_event!(self, RadarrEvent::GetReleases(Some(movie_id)));
}
RadarrCommand::SearchNewMovie { query } => {
execute_network_event!(self, RadarrEvent::SearchNewMovie(Some(query)));
}
RadarrCommand::StartTask { task_name } => {
execute_network_event!(self, RadarrEvent::StartTask(Some(task_name)));
}
RadarrCommand::TestIndexer { indexer_id } => {
execute_network_event!(self, RadarrEvent::TestIndexer(Some(indexer_id)));
}
RadarrCommand::TestAllIndexers => {
execute_network_event!(self, RadarrEvent::TestAllIndexers);
}
RadarrCommand::TriggerAutomaticSearch { movie_id } => {
execute_network_event!(self, RadarrEvent::TriggerAutomaticSearch(Some(movie_id)));
}
}
Ok(())
}
}
+702
View File
@@ -0,0 +1,702 @@
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use clap::CommandFactory;
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_command_from() {
let command = RadarrCommand::TestAllIndexers;
let result = Command::from(command.clone());
assert_eq!(result, Command::Radarr(command));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_commands_that_have_no_arg_requirements(
#[values("clear-blocklist", "test-all-indexers")] subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", subcommand]);
assert!(result.is_ok());
}
#[rstest]
fn test_download_release_requires_movie_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"download-release",
"--indexer-id",
"1",
"--guid",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_download_release_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"download-release",
"--indexer-id",
"1",
"--movie-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_download_release_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"download-release",
"--guid",
"1",
"--movie-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"download-release",
"--guid",
"1",
"--movie-id",
"1",
"--indexer-id",
"1",
]);
assert!(result.is_ok());
}
#[rstest]
fn test_manual_search_requires_movie_id() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "manual-search"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"manual-search",
"--movie-id",
"1",
]);
assert!(result.is_ok());
}
#[rstest]
fn test_search_new_movie_requires_query() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "search-new-movie"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_search_new_movie_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"search-new-movie",
"--query",
"halo",
]);
assert!(result.is_ok());
}
#[rstest]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "start-task"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_start_task_task_name_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"start-task",
"--task-name",
"test",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_start_task_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"start-task",
"--task-name",
"application-check-update",
]);
assert!(result.is_ok());
}
#[rstest]
fn test_test_indexer_requires_indexer_id() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "test-indexer"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_test_indexer_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"test-indexer",
"--indexer-id",
"1",
]);
assert!(result.is_ok());
}
#[rstest]
fn test_trigger_automatic_search_requires_movie_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "trigger-automatic-search"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"trigger-automatic-search",
"--movie-id",
"1",
]);
assert!(result.is_ok());
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
radarr::{
add_command_handler::RadarrAddCommand, delete_command_handler::RadarrDeleteCommand,
edit_command_handler::RadarrEditCommand, get_command_handler::RadarrGetCommand,
list_command_handler::RadarrListCommand, refresh_command_handler::RadarrRefreshCommand,
RadarrCliHandler, RadarrCommand,
},
CliCommandHandler,
},
models::{
radarr_models::{
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrSerdeable, ReleaseDownloadBody,
TaskName,
},
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_clear_blocklist_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(RadarrEvent::GetBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse(
BlocklistResponse {
records: vec![BlocklistItem::default()],
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(RadarrEvent::ClearBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let claer_blocklist_command = RadarrCommand::ClearBlocklist;
let result = RadarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_download_release_command() {
let expected_release_download_body = ReleaseDownloadBody {
guid: "guid".to_owned(),
indexer_id: 1,
movie_id: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::DownloadRelease(Some(expected_release_download_body)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let download_release_command = RadarrCommand::DownloadRelease {
guid: "guid".to_owned(),
indexer_id: 1,
movie_id: 1,
};
let result = RadarrCliHandler::with(&app_arc, download_release_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_manual_search_command() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetReleases(Some(expected_movie_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let manual_search_command = RadarrCommand::ManualSearch { movie_id: 1 };
let result = RadarrCliHandler::with(&app_arc, manual_search_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_search_new_movie_command() {
let expected_search_query = "halo".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::SearchNewMovie(Some(expected_search_query)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let search_new_movie_command = RadarrCommand::SearchNewMovie {
query: "halo".to_owned(),
};
let result = RadarrCliHandler::with(&app_arc, search_new_movie_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_start_task_command() {
let expected_task_name = TaskName::ApplicationCheckUpdate;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::StartTask(Some(expected_task_name)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let start_task_command = RadarrCommand::StartTask {
task_name: TaskName::ApplicationCheckUpdate,
};
let result = RadarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_test_indexer_command() {
let expected_indexer_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::TestIndexer(Some(expected_indexer_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let test_indexer_command = RadarrCommand::TestIndexer { indexer_id: 1 };
let result = RadarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_test_all_indexers_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(RadarrEvent::TestAllIndexers.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let test_all_indexers_command = RadarrCommand::TestAllIndexers;
let result = RadarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_trigger_automatic_search_command() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::TriggerAutomaticSearch(Some(expected_movie_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let trigger_automatic_search_command = RadarrCommand::TriggerAutomaticSearch { movie_id: 1 };
let result = RadarrCliHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_radarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let add_tag_command = RadarrCommand::Add(RadarrAddCommand::Tag {
name: expected_tag_name,
});
let result = RadarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_radarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
let expected_blocklist_item_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_blocklist_item_command =
RadarrCommand::Delete(RadarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1,
});
let result =
RadarrCliHandler::with(&app_arc, delete_blocklist_item_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_radarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
let expected_edit_all_indexer_settings = IndexerSettings {
allow_hardcoded_subs: true,
availability_delay: 1,
id: 1,
maximum_size: 1,
minimum_age: 1,
prefer_indexer_flags: true,
retention: 1,
rss_sync_interval: 1,
whitelisted_hardcoded_subs: "test".into(),
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
IndexerSettings {
allow_hardcoded_subs: false,
availability_delay: 2,
id: 1,
maximum_size: 2,
minimum_age: 2,
prefer_indexer_flags: false,
retention: 2,
rss_sync_interval: 2,
whitelisted_hardcoded_subs: "testing".into(),
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let edit_all_indexer_settings_command =
RadarrCommand::Edit(RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs: true,
disable_allow_hardcoded_subs: false,
availability_delay: Some(1),
maximum_size: Some(1),
minimum_age: Some(1),
prefer_indexer_flags: true,
disable_prefer_indexer_flags: false,
retention: Some(1),
rss_sync_interval: Some(1),
whitelisted_subtitle_tags: Some("test".to_owned()),
});
let result = RadarrCliHandler::with(
&app_arc,
edit_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_radarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_all_indexer_settings_command =
RadarrCommand::Get(RadarrGetCommand::AllIndexerSettings);
let result = RadarrCliHandler::with(
&app_arc,
get_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_radarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetMovieCredits(Some(expected_movie_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_movie_credits_command =
RadarrCommand::List(RadarrListCommand::MovieCredits { movie_id: 1 });
let result = RadarrCliHandler::with(&app_arc, list_movie_credits_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_radarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::UpdateAndScan(Some(expected_movie_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let refresh_movie_command =
RadarrCommand::Refresh(RadarrRefreshCommand::Movie { movie_id: 1 });
let result = RadarrCliHandler::with(&app_arc, refresh_movie_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+84
View File
@@ -0,0 +1,84 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait},
};
use super::RadarrCommand;
#[cfg(test)]
#[path = "refresh_command_handler_tests.rs"]
mod refresh_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RadarrRefreshCommand {
#[command(about = "Refresh all movie data for all movies in your library")]
AllMovies,
#[command(about = "Refresh movie data and scan disk for the movie with the given ID")]
Movie {
#[arg(
long,
help = "The ID of the movie to refresh information on and to scan the disk for",
required = true
)]
movie_id: i64,
},
#[command(about = "Refresh all collection data for all collections in your library")]
Collections,
#[command(about = "Refresh all downloads in Radarr")]
Downloads,
}
impl From<RadarrRefreshCommand> for Command {
fn from(value: RadarrRefreshCommand) -> Self {
Command::Radarr(RadarrCommand::Refresh(value))
}
}
pub(super) struct RadarrRefreshCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand>
for RadarrRefreshCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: RadarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
RadarrRefreshCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<()> {
match self.command {
RadarrRefreshCommand::AllMovies => {
execute_network_event!(self, RadarrEvent::UpdateAllMovies);
}
RadarrRefreshCommand::Collections => {
execute_network_event!(self, RadarrEvent::UpdateCollections);
}
RadarrRefreshCommand::Downloads => {
execute_network_event!(self, RadarrEvent::UpdateDownloads);
}
RadarrRefreshCommand::Movie { movie_id } => {
execute_network_event!(self, RadarrEvent::UpdateAndScan(Some(movie_id)));
}
}
Ok(())
}
}
@@ -0,0 +1,133 @@
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use clap::CommandFactory;
use crate::cli::radarr::refresh_command_handler::RadarrRefreshCommand;
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
#[test]
fn test_radarr_refresh_command_from() {
let command = RadarrRefreshCommand::AllMovies;
let result = Command::from(command.clone());
assert_eq!(result, Command::Radarr(RadarrCommand::Refresh(command)));
}
mod cli {
use super::*;
use clap::Parser;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_refresh_commands_have_no_arg_requirements(
#[values("all-movies", "collections", "downloads")] subcommand: &str,
) {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "refresh", subcommand]);
assert!(result.is_ok());
}
#[test]
fn test_refresh_movie_requires_movie_id() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "refresh", "movie"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_refresh_movie_success() {
let expected_args = RadarrRefreshCommand::Movie { movie_id: 1 };
let result =
Cli::try_parse_from(["managarr", "radarr", "refresh", "movie", "--movie-id", "1"]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Refresh(refresh_command))) =
result.unwrap().command
{
assert_eq!(refresh_command, expected_args);
}
}
}
mod handler {
use rstest::rstest;
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::CliCommandHandler;
use crate::{
app::App,
cli::radarr::refresh_command_handler::{RadarrRefreshCommand, RadarrRefreshCommandHandler},
models::{radarr_models::RadarrSerdeable, Serdeable},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
};
#[rstest]
#[case(RadarrRefreshCommand::AllMovies, RadarrEvent::UpdateAllMovies)]
#[case(RadarrRefreshCommand::Collections, RadarrEvent::UpdateCollections)]
#[case(RadarrRefreshCommand::Downloads, RadarrEvent::UpdateDownloads)]
#[tokio::test]
async fn test_handle_list_blocklist_command(
#[case] refresh_command: RadarrRefreshCommand,
#[case] expected_radarr_event: RadarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(expected_radarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let result = RadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_refresh_movie_command() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::UpdateAndScan(Some(expected_movie_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let refresh_movie_command = RadarrRefreshCommand::Movie { movie_id: 1 };
let result =
RadarrRefreshCommandHandler::with(&app_arc, refresh_movie_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
@@ -410,7 +410,7 @@ mod tests {
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
RadarrEvent::DeleteBlocklistItem
RadarrEvent::DeleteBlocklistItem(None)
)]
#[case(
ActiveRadarrBlock::Blocklist,
@@ -132,7 +132,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
match self.active_radarr_block {
ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem);
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteBlocklistItem(None));
}
self.app.pop_navigation_stack();
@@ -1,5 +1,6 @@
#[cfg(test)]
mod tests {
use core::sync::atomic::Ordering::SeqCst;
use std::cmp::Ordering;
use std::iter;
@@ -231,7 +232,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.collections
@@ -239,7 +240,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
4
);
@@ -252,7 +253,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.collections
@@ -260,7 +261,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
0
);
}
@@ -284,7 +285,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.collections
@@ -292,7 +293,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
4
);
@@ -305,7 +306,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.collections
@@ -313,7 +314,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
0
);
}
@@ -458,7 +459,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.collections
@@ -466,7 +467,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
1
);
@@ -479,7 +480,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.collections
@@ -487,7 +488,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
0
);
}
@@ -506,7 +507,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.collections
@@ -514,7 +515,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
1
);
@@ -527,7 +528,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.collections
@@ -535,7 +536,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
0
);
}
@@ -192,7 +192,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
match self.app.data.radarr_data.selected_block.get_active_block() {
ActiveRadarrBlock::EditCollectionConfirmPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection);
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::EditCollection(None));
self.app.should_refresh = true;
}
@@ -200,6 +200,8 @@ mod tests {
}
mod test_handle_home_end {
use std::sync::atomic::Ordering;
use pretty_assertions::assert_eq;
use strum::IntoEnumIterator;
@@ -337,7 +339,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_collection_modal
@@ -345,7 +347,7 @@ mod tests {
.unwrap()
.path
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -358,7 +360,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_collection_modal
@@ -366,13 +368,15 @@ mod tests {
.unwrap()
.path
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
}
mod test_handle_left_right_action {
use std::sync::atomic::Ordering;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use rstest::rstest;
@@ -420,7 +424,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_collection_modal
@@ -428,7 +432,7 @@ mod tests {
.unwrap()
.path
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -441,7 +445,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_collection_modal
@@ -449,7 +453,7 @@ mod tests {
.unwrap()
.path
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -561,7 +565,7 @@ mod tests {
);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditCollection)
Some(RadarrEvent::EditCollection(None))
);
assert!(app.should_refresh);
}
@@ -247,7 +247,7 @@ mod tests {
#[case(
ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::DeleteDownloadPrompt,
RadarrEvent::DeleteDownload
RadarrEvent::DeleteDownload(None)
)]
#[case(
ActiveRadarrBlock::Downloads,
@@ -91,7 +91,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
match self.active_radarr_block {
ActiveRadarrBlock::DeleteDownloadPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload);
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None));
}
self.app.pop_navigation_stack();
@@ -275,7 +275,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
ActiveRadarrBlock::EditIndexerConfirmPrompt => {
let radarr_data = &mut self.app.data.radarr_data;
if radarr_data.prompt_confirm {
radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer);
radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(None));
self.app.should_refresh = true;
} else {
radarr_data.edit_indexer_modal = None;
@@ -66,6 +66,8 @@ mod tests {
}
mod test_handle_home_end {
use std::sync::atomic::Ordering;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use pretty_assertions::assert_eq;
@@ -89,7 +91,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -97,7 +99,7 @@ mod tests {
.unwrap()
.name
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -110,7 +112,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -118,7 +120,7 @@ mod tests {
.unwrap()
.name
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -140,7 +142,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -148,7 +150,7 @@ mod tests {
.unwrap()
.url
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -161,7 +163,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -169,7 +171,7 @@ mod tests {
.unwrap()
.url
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -191,7 +193,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -199,7 +201,7 @@ mod tests {
.unwrap()
.api_key
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -212,7 +214,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -220,7 +222,7 @@ mod tests {
.unwrap()
.api_key
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -242,7 +244,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -250,7 +252,7 @@ mod tests {
.unwrap()
.seed_ratio
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -263,7 +265,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -271,7 +273,7 @@ mod tests {
.unwrap()
.seed_ratio
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -293,7 +295,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -301,7 +303,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -314,7 +316,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -322,13 +324,15 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
}
mod test_handle_left_right_action {
use std::sync::atomic::Ordering;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{
@@ -511,7 +515,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -519,7 +523,7 @@ mod tests {
.unwrap()
.name
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -532,7 +536,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -540,7 +544,7 @@ mod tests {
.unwrap()
.name
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -562,7 +566,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -570,7 +574,7 @@ mod tests {
.unwrap()
.url
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -583,7 +587,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -591,7 +595,7 @@ mod tests {
.unwrap()
.url
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -613,7 +617,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -621,7 +625,7 @@ mod tests {
.unwrap()
.api_key
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -634,7 +638,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -642,7 +646,7 @@ mod tests {
.unwrap()
.api_key
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -664,7 +668,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -672,7 +676,7 @@ mod tests {
.unwrap()
.seed_ratio
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -685,7 +689,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -693,7 +697,7 @@ mod tests {
.unwrap()
.seed_ratio
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -715,7 +719,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -723,7 +727,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -736,7 +740,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_indexer_modal
@@ -744,7 +748,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -821,7 +825,7 @@ mod tests {
assert!(app.should_refresh);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditIndexer)
Some(RadarrEvent::EditIndexer(None))
);
}
@@ -49,7 +49,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
fn handle_scroll_up(&mut self) {
let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap();
match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsPrompt => {
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
self.app.data.radarr_data.selected_block.previous();
}
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => {
@@ -74,7 +74,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
fn handle_scroll_down(&mut self) {
let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap();
match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsPrompt => self.app.data.radarr_data.selected_block.next(),
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
self.app.data.radarr_data.selected_block.next()
}
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => {
if indexer_settings.minimum_age > 0 {
indexer_settings.minimum_age -= 1;
@@ -134,7 +136,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
fn handle_left_right_action(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsPrompt => {
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::IndexerSettingsConfirmPrompt
{
@@ -165,12 +167,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
fn handle_submit(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsPrompt => {
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
match self.app.data.radarr_data.selected_block.get_active_block() {
ActiveRadarrBlock::IndexerSettingsConfirmPrompt => {
let radarr_data = &mut self.app.data.radarr_data;
if radarr_data.prompt_confirm {
radarr_data.prompt_confirm_action = Some(RadarrEvent::EditAllIndexerSettings);
radarr_data.prompt_confirm_action = Some(RadarrEvent::EditAllIndexerSettings(None));
self.app.should_refresh = true;
} else {
radarr_data.indexer_settings = None;
@@ -225,7 +227,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
fn handle_esc(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsPrompt => {
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
self.app.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false;
self.app.data.radarr_data.indexer_settings = None;
@@ -104,7 +104,7 @@ mod tests {
IndexerSettingsHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -136,7 +136,7 @@ mod tests {
IndexerSettingsHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -201,6 +201,8 @@ mod tests {
}
mod test_handle_home_end {
use std::sync::atomic::Ordering;
use pretty_assertions::assert_eq;
use crate::models::radarr_models::IndexerSettings;
@@ -224,7 +226,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.indexer_settings
@@ -232,7 +234,7 @@ mod tests {
.unwrap()
.whitelisted_hardcoded_subs
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -245,7 +247,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.indexer_settings
@@ -253,13 +255,15 @@ mod tests {
.unwrap()
.whitelisted_hardcoded_subs
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
}
mod test_handle_left_right_action {
use std::sync::atomic::Ordering;
use crate::models::radarr_models::IndexerSettings;
use crate::models::servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
@@ -278,7 +282,7 @@ mod tests {
IndexerSettingsHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -288,7 +292,7 @@ mod tests {
IndexerSettingsHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -336,7 +340,7 @@ mod tests {
IndexerSettingsHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -349,7 +353,7 @@ mod tests {
IndexerSettingsHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -377,7 +381,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.indexer_settings
@@ -385,7 +389,7 @@ mod tests {
.unwrap()
.whitelisted_hardcoded_subs
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -398,7 +402,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.indexer_settings
@@ -406,7 +410,7 @@ mod tests {
.unwrap()
.whitelisted_hardcoded_subs
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -432,7 +436,7 @@ mod tests {
fn test_edit_indexer_settings_prompt_prompt_decline_submit() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app
@@ -445,7 +449,7 @@ mod tests {
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -460,7 +464,7 @@ mod tests {
fn test_edit_indexer_settings_prompt_prompt_confirmation_submit() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app
@@ -474,7 +478,7 @@ mod tests {
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -482,7 +486,7 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditAllIndexerSettings)
Some(RadarrEvent::EditAllIndexerSettings(None))
);
assert!(app.data.radarr_data.indexer_settings.is_some());
assert!(app.should_refresh);
@@ -493,21 +497,21 @@ mod tests {
let mut app = App::default();
app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.radarr_data.prompt_confirm = true;
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(!app.should_refresh);
}
@@ -524,7 +528,7 @@ mod tests {
) {
let mut app = App::default();
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(index);
@@ -532,7 +536,7 @@ mod tests {
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -547,7 +551,7 @@ mod tests {
let mut app = App::default();
app.is_loading = true;
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(index);
@@ -555,14 +559,14 @@ mod tests {
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
}
@@ -570,7 +574,7 @@ mod tests {
fn test_edit_indexer_settings_prompt_submit_whitelisted_subtitle_tags_input() {
let mut app = App::default();
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(7);
@@ -578,7 +582,7 @@ mod tests {
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -597,19 +601,19 @@ mod tests {
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(3);
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(
app
@@ -624,14 +628,14 @@ mod tests {
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(
!app
@@ -651,19 +655,19 @@ mod tests {
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(8);
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(
app
@@ -678,14 +682,14 @@ mod tests {
IndexerSettingsHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(
!app
@@ -706,7 +710,7 @@ mod tests {
whitelisted_hardcoded_subs: "Test tags".into(),
..IndexerSettings::default()
});
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.push_navigation_stack(
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into(),
);
@@ -731,7 +735,7 @@ mod tests {
.is_empty());
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
}
@@ -748,14 +752,14 @@ mod tests {
) {
let mut app = App::default();
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.push_navigation_stack(active_radarr_block.into());
IndexerSettingsHandler::with(&SUBMIT_KEY, &mut app, &active_radarr_block, &None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
}
}
@@ -775,13 +779,13 @@ mod tests {
let mut app = App::default();
app.is_loading = is_ready;
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(
&ESC_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
@@ -926,7 +930,7 @@ mod tests {
let handler = IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
);
@@ -941,7 +945,7 @@ mod tests {
let handler = IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
);
@@ -957,7 +961,7 @@ mod tests {
let handler = IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsPrompt,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
);
@@ -375,7 +375,7 @@ mod tests {
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteIndexer)
Some(RadarrEvent::DeleteIndexer(None))
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
}
@@ -577,7 +577,7 @@ mod tests {
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsPrompt.into()
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert_eq!(
app.data.radarr_data.selected_block.blocks,
@@ -724,7 +724,7 @@ mod tests {
#[rstest]
fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler(
#[values(
ActiveRadarrBlock::IndexerSettingsPrompt,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
ActiveRadarrBlock::IndexerSettingsConfirmPrompt,
ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
+2 -2
View File
@@ -121,7 +121,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
ActiveRadarrBlock::DeleteIndexerPrompt => {
let radarr_data = &mut self.app.data.radarr_data;
if radarr_data.prompt_confirm {
radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer);
radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(None));
}
self.app.pop_navigation_stack();
@@ -189,7 +189,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
_ if *key == DEFAULT_KEYBINDINGS.settings.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::IndexerSettingsPrompt.into());
.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
}
@@ -367,7 +367,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
match self.app.data.radarr_data.selected_block.get_active_block() {
ActiveRadarrBlock::AddMovieConfirmPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie);
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None));
}
self.app.pop_navigation_stack();
@@ -416,6 +416,8 @@ mod tests {
}
mod test_handle_home_end {
use std::sync::atomic::Ordering;
use strum::IntoEnumIterator;
use crate::extended_stateful_iterable_vec;
@@ -769,14 +771,14 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.add_movie_search
.as_ref()
.unwrap()
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -789,14 +791,14 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.add_movie_search
.as_ref()
.unwrap()
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -818,7 +820,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.add_movie_modal
@@ -826,7 +828,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -839,7 +841,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.add_movie_modal
@@ -847,13 +849,15 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
}
mod test_handle_left_right_action {
use std::sync::atomic::Ordering;
use crate::models::servarr_data::radarr::modals::AddMovieModal;
use rstest::rstest;
@@ -886,14 +890,14 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.add_movie_search
.as_ref()
.unwrap()
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -906,14 +910,14 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.add_movie_search
.as_ref()
.unwrap()
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -935,7 +939,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.add_movie_modal
@@ -943,7 +947,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -956,7 +960,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.add_movie_modal
@@ -964,7 +968,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -1211,7 +1215,7 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::AddMovie)
Some(RadarrEvent::AddMovie(None))
);
assert!(app.data.radarr_data.add_movie_modal.is_some());
}
@@ -71,7 +71,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
match self.app.data.radarr_data.selected_block.get_active_block() {
ActiveRadarrBlock::DeleteMovieConfirmPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie);
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(None));
self.app.should_refresh = true;
} else {
self.app.data.radarr_data.reset_delete_movie_preferences();
@@ -150,7 +150,7 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteMovie)
Some(RadarrEvent::DeleteMovie(None))
);
assert!(app.should_refresh);
assert!(app.data.radarr_data.prompt_confirm);
@@ -222,7 +222,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
match self.app.data.radarr_data.selected_block.get_active_block() {
ActiveRadarrBlock::EditMovieConfirmPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie);
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(None));
self.app.should_refresh = true;
}
@@ -182,6 +182,8 @@ mod tests {
}
mod test_handle_home_end {
use std::sync::atomic::Ordering;
use strum::IntoEnumIterator;
use crate::models::servarr_data::radarr::modals::EditMovieModal;
@@ -318,7 +320,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_movie_modal
@@ -326,7 +328,7 @@ mod tests {
.unwrap()
.path
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -339,7 +341,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_movie_modal
@@ -347,7 +349,7 @@ mod tests {
.unwrap()
.path
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -369,7 +371,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_movie_modal
@@ -377,7 +379,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -390,7 +392,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_movie_modal
@@ -398,13 +400,15 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
}
mod test_handle_left_right_action {
use std::sync::atomic::Ordering;
use crate::models::servarr_data::radarr::modals::EditMovieModal;
use rstest::rstest;
@@ -440,7 +444,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_movie_modal
@@ -448,7 +452,7 @@ mod tests {
.unwrap()
.path
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -461,7 +465,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_movie_modal
@@ -469,7 +473,7 @@ mod tests {
.unwrap()
.path
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -491,7 +495,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_movie_modal
@@ -499,7 +503,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -512,7 +516,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_movie_modal
@@ -520,7 +524,7 @@ mod tests {
.unwrap()
.tags
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -661,7 +665,7 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditMovie)
Some(RadarrEvent::EditMovie(None))
);
assert!(app.data.radarr_data.edit_movie_modal.is_some());
assert!(app.should_refresh);
@@ -1,5 +1,6 @@
#[cfg(test)]
mod tests {
use core::sync::atomic::Ordering::SeqCst;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use std::cmp::Ordering;
@@ -213,7 +214,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.movies
@@ -221,7 +222,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
4
);
@@ -234,7 +235,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.movies
@@ -242,7 +243,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
0
);
}
@@ -266,7 +267,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.movies
@@ -274,7 +275,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
4
);
@@ -287,7 +288,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.movies
@@ -295,7 +296,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
0
);
}
@@ -488,7 +489,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.movies
@@ -496,7 +497,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
1
);
@@ -509,7 +510,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.movies
@@ -517,7 +518,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
0
);
}
@@ -536,7 +537,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.movies
@@ -544,7 +545,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
1
);
@@ -557,7 +558,7 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.movies
@@ -565,7 +566,7 @@ mod tests {
.as_ref()
.unwrap()
.offset
.borrow(),
.load(SeqCst),
0
);
}
@@ -349,14 +349,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
ActiveRadarrBlock::AutomaticallySearchMoviePrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::TriggerAutomaticSearch);
Some(RadarrEvent::TriggerAutomaticSearch(None));
}
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::UpdateAndScanPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan);
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None));
}
self.app.pop_navigation_stack();
@@ -368,7 +368,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
}
ActiveRadarrBlock::ManualSearchConfirmPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease);
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DownloadRelease(None));
}
self.app.pop_navigation_stack();
@@ -1292,12 +1292,15 @@ mod tests {
#[rstest]
#[case(
ActiveRadarrBlock::AutomaticallySearchMoviePrompt,
RadarrEvent::TriggerAutomaticSearch
RadarrEvent::TriggerAutomaticSearch(None)
)]
#[case(
ActiveRadarrBlock::UpdateAndScanPrompt,
RadarrEvent::UpdateAndScan(None)
)]
#[case(ActiveRadarrBlock::UpdateAndScanPrompt, RadarrEvent::UpdateAndScan)]
#[case(
ActiveRadarrBlock::ManualSearchConfirmPrompt,
RadarrEvent::DownloadRelease
RadarrEvent::DownloadRelease(None)
)]
fn test_movie_info_prompt_confirm_submit(
#[case] prompt_block: ActiveRadarrBlock,
@@ -140,7 +140,7 @@ mod tests {
#[values(
ActiveRadarrBlock::DeleteIndexerPrompt,
ActiveRadarrBlock::Indexers,
ActiveRadarrBlock::IndexerSettingsPrompt,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
ActiveRadarrBlock::IndexerSettingsConfirmPrompt,
ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
@@ -115,7 +115,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
match self.active_radarr_block {
ActiveRadarrBlock::DeleteRootFolderPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteRootFolder);
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteRootFolder(None));
}
self.app.pop_navigation_stack();
@@ -131,7 +132,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
.text
.is_empty() =>
{
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddRootFolder);
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddRootFolder(None));
self.app.data.radarr_data.prompt_confirm = true;
self.app.should_ignore_quit_key = false;
self.app.pop_navigation_stack();
@@ -59,6 +59,8 @@ mod tests {
}
mod test_handle_home_end {
use std::sync::atomic::Ordering;
use pretty_assertions::assert_eq;
use crate::models::radarr_models::RootFolder;
@@ -132,14 +134,14 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_root_folder
.as_ref()
.unwrap()
.offset
.borrow(),
.load(Ordering::SeqCst),
4
);
@@ -152,14 +154,14 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_root_folder
.as_ref()
.unwrap()
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -222,6 +224,8 @@ mod tests {
}
mod test_handle_left_right_action {
use std::sync::atomic::Ordering;
use pretty_assertions::assert_eq;
use rstest::rstest;
@@ -313,14 +317,14 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_root_folder
.as_ref()
.unwrap()
.offset
.borrow(),
.load(Ordering::SeqCst),
1
);
@@ -333,14 +337,14 @@ mod tests {
.handle();
assert_eq!(
*app
app
.data
.radarr_data
.edit_root_folder
.as_ref()
.unwrap()
.offset
.borrow(),
.load(Ordering::SeqCst),
0
);
}
@@ -381,7 +385,7 @@ mod tests {
assert!(!app.should_ignore_quit_key);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::AddRootFolder)
Some(RadarrEvent::AddRootFolder(None))
);
assert_eq!(
app.get_current_route(),
@@ -438,7 +442,7 @@ mod tests {
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteRootFolder)
Some(RadarrEvent::DeleteRootFolder(None))
);
assert_eq!(
app.get_current_route(),
@@ -136,7 +136,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
}
ActiveRadarrBlock::SystemTaskStartConfirmPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask);
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(None));
}
self.app.pop_navigation_stack();
@@ -717,7 +717,7 @@ mod tests {
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::StartTask)
Some(RadarrEvent::StartTask(None))
);
assert_eq!(
app.get_current_route(),
+85 -11
View File
@@ -1,14 +1,23 @@
#![warn(rust_2018_idioms)]
use std::panic::PanicInfo;
use std::panic::PanicHookInfo;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::{io, panic};
use std::{io, panic, process};
use anyhow::anyhow;
use anyhow::Result;
use clap::{
command, crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser,
};
use clap_complete::generate;
use colored::Colorize;
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode, size, EnterAlternateScreen, LeaveAlternateScreen,
};
use log::error;
use network::NetworkTrait;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use tokio::sync::mpsc::Receiver;
@@ -16,12 +25,14 @@ use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken;
use crate::app::App;
use crate::cli::Command;
use crate::event::input_event::{Events, InputEvent};
use crate::event::Key;
use crate::network::{Network, NetworkEvent};
use crate::ui::ui;
mod app;
mod cli;
mod event;
mod handlers;
mod logos;
@@ -30,16 +41,49 @@ mod network;
mod ui;
mod utils;
static MIN_TERM_WIDTH: u16 = 205;
static MIN_TERM_HEIGHT: u16 = 40;
#[derive(Debug, Parser)]
#[command(
name = crate_name!(),
author = crate_authors!(),
version = crate_version!(),
about = crate_description!(),
help_template = "\
{before-help}{name} {version}
{author-with-newline}
{about-with-newline}
{usage-heading} {usage}
{all-args}{after-help}
"
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
}
#[tokio::main]
async fn main() -> Result<()> {
log4rs::init_config(utils::init_logging_config())?;
panic::set_hook(Box::new(|info| {
panic_hook(info);
}));
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let args = Cli::parse();
let config = confy::load("managarr", "config")?;
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
let cancellation_token = CancellationToken::new();
let ctrlc_cancellation_token = cancellation_token.clone();
ctrlc::set_handler(move || {
ctrlc_cancellation_token.cancel();
r.store(false, Ordering::SeqCst);
process::exit(1);
})
.expect("Error setting Ctrl-C handler");
let app = Arc::new(Mutex::new(App::new(
sync_network_tx,
@@ -47,11 +91,28 @@ async fn main() -> Result<()> {
cancellation_token.clone(),
)));
let app_nw = Arc::clone(&app);
match args.command {
Some(command) => match command {
Command::Radarr(_) => {
let app_nw = Arc::clone(&app);
let mut network = Network::new(&app_nw, cancellation_token);
std::thread::spawn(move || start_networking(sync_network_rx, &app_nw, cancellation_token));
start_ui(&app).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 } => {
let mut cli = Cli::command();
generate(shell, &mut cli, "managarr", &mut io::stdout())
}
},
None => {
let app_nw = Arc::clone(&app);
std::thread::spawn(move || start_networking(sync_network_rx, &app_nw, cancellation_token));
start_ui(&app).await?;
}
}
Ok(())
}
@@ -65,11 +126,24 @@ async fn start_networking(
let mut network = Network::new(app, cancellation_token);
while let Some(network_event) = network_rx.recv().await {
network.handle_network_event(network_event).await;
if let Err(e) = network.handle_network_event(network_event).await {
error!("Encountered an error handling network event: {e:?}");
}
}
}
async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> {
let (width, height) = size()?;
if width < MIN_TERM_WIDTH || height < MIN_TERM_HEIGHT {
return Err(anyhow!(
"Terminal too small. Minimum size required: {}x{}; current terminal size: {}x{}",
MIN_TERM_WIDTH,
MIN_TERM_HEIGHT,
width,
height
));
}
let mut stdout = io::stdout();
enable_raw_mode()?;
@@ -111,7 +185,7 @@ async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> {
}
#[cfg(debug_assertions)]
fn panic_hook(info: &PanicInfo<'_>) {
fn panic_hook(info: &PanicHookInfo<'_>) {
use backtrace::Backtrace;
use crossterm::style::Print;
@@ -139,7 +213,7 @@ fn panic_hook(info: &PanicInfo<'_>) {
}
#[cfg(not(debug_assertions))]
fn panic_hook(info: &PanicInfo<'_>) {
fn panic_hook(info: &PanicHookInfo<'_>) {
use human_panic::{handle_dump, print_msg, Metadata};
let meta = Metadata {
+60 -20
View File
@@ -1,11 +1,11 @@
use std::cell::RefCell;
use std::fmt::{Debug, Display, Formatter};
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use radarr_models::RadarrSerdeable;
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Number;
pub mod radarr_models;
pub mod servarr_data;
pub mod stateful_list;
@@ -29,6 +29,12 @@ pub enum Route {
Tautulli,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum Serdeable {
Radarr(RadarrSerdeable),
}
pub trait Scrollable {
fn scroll_down(&mut self);
fn scroll_up(&mut self);
@@ -88,19 +94,42 @@ impl Scrollable for ScrollableText {
}
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Deserialize, Debug)]
#[serde(from = "String")]
pub struct HorizontallyScrollableText {
pub text: String,
pub offset: RefCell<usize>,
pub offset: AtomicUsize,
}
impl Clone for HorizontallyScrollableText {
fn clone(&self) -> Self {
HorizontallyScrollableText {
text: self.text.clone(),
offset: AtomicUsize::new(self.offset.load(Ordering::SeqCst)),
}
}
}
impl PartialEq for HorizontallyScrollableText {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
}
}
impl Eq for HorizontallyScrollableText {}
impl From<String> for HorizontallyScrollableText {
fn from(text: String) -> HorizontallyScrollableText {
HorizontallyScrollableText::new(text)
}
}
impl From<&String> for HorizontallyScrollableText {
fn from(text: &String) -> HorizontallyScrollableText {
HorizontallyScrollableText::new(text.clone())
}
}
impl From<&str> for HorizontallyScrollableText {
fn from(text: &str) -> HorizontallyScrollableText {
HorizontallyScrollableText::new(text.to_owned())
@@ -109,14 +138,14 @@ impl From<&str> for HorizontallyScrollableText {
impl Display for HorizontallyScrollableText {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if *self.offset.borrow() == 0 {
if self.offset.load(Ordering::SeqCst) == 0 {
write!(f, "{}", self.text)
} else {
let text_vec = self.text.chars().collect::<Vec<_>>();
write!(
f,
"{}",
text_vec[*self.offset.borrow()..]
text_vec[self.offset.load(Ordering::SeqCst)..]
.iter()
.cloned()
.collect::<String>()
@@ -138,7 +167,7 @@ impl HorizontallyScrollableText {
pub fn new(text: String) -> HorizontallyScrollableText {
HorizontallyScrollableText {
text,
offset: RefCell::new(0),
offset: AtomicUsize::new(0),
}
}
@@ -147,46 +176,44 @@ impl HorizontallyScrollableText {
}
pub fn scroll_left(&self) {
if *self.offset.borrow() < self.len() {
let new_offset = *self.offset.borrow() + 1;
*self.offset.borrow_mut() = new_offset;
if self.offset.load(Ordering::SeqCst) < self.len() {
self.offset.fetch_add(1, Ordering::SeqCst);
}
}
pub fn scroll_right(&self) {
if *self.offset.borrow() > 0 {
let new_offset = *self.offset.borrow() - 1;
*self.offset.borrow_mut() = new_offset;
if self.offset.load(Ordering::SeqCst) > 0 {
self.offset.fetch_sub(1, Ordering::SeqCst);
}
}
pub fn scroll_home(&self) {
*self.offset.borrow_mut() = self.len();
self.offset.store(self.len(), Ordering::SeqCst);
}
pub fn reset_offset(&self) {
*self.offset.borrow_mut() = 0;
self.offset.store(0, Ordering::SeqCst);
}
pub fn scroll_left_or_reset(&self, width: usize, is_current_selection: bool, can_scroll: bool) {
if can_scroll && is_current_selection && self.len() >= width {
if *self.offset.borrow() < self.len() {
if self.offset.load(Ordering::SeqCst) < self.len() {
self.scroll_left();
} else {
self.reset_offset();
}
} else if *self.offset.borrow() != 0 && !is_current_selection {
} else if self.offset.load(Ordering::SeqCst) != 0 && !is_current_selection {
self.reset_offset();
}
}
pub fn pop(&mut self) {
if *self.offset.borrow() < self.len() {
if self.offset.load(Ordering::SeqCst) < self.len() {
let (index, _) = self
.text
.chars()
.enumerate()
.nth(self.len() - *self.offset.borrow() - 1)
.nth(self.len() - self.offset.load(Ordering::SeqCst) - 1)
.unwrap();
self.text = self
.text
@@ -202,7 +229,7 @@ impl HorizontallyScrollableText {
if self.text.is_empty() {
self.text.push(character);
} else {
let index = self.len() - *self.offset.borrow();
let index = self.len() - self.offset.load(Ordering::SeqCst);
if index == self.len() {
self.text.push(character);
@@ -338,3 +365,16 @@ pub fn strip_non_search_characters(input: &str) -> String {
.replace_all(&input.to_lowercase(), "")
.to_string()
}
#[macro_export]
macro_rules! serde_enum_from {
($enum_name:ident { $($variant:ident($ty:ty),)* }) => {
$(
impl From<$ty> for $enum_name {
fn from(value: $ty) -> Self {
$enum_name::$variant(value)
}
}
)*
}
}
+151 -40
View File
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use pretty_assertions::{assert_eq, assert_str_eq};
use serde::de::value::Error as ValueError;
@@ -100,7 +101,22 @@ mod tests {
let test_text = "Test string";
let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned());
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
assert_str_eq!(horizontally_scrollable_text.text, test_text);
}
#[test]
fn test_horizontally_scrollable_text_from_string_ref() {
let test_text = "Test string".to_owned();
let horizontally_scrollable_text = HorizontallyScrollableText::from(&test_text);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
assert_str_eq!(horizontally_scrollable_text.text, test_text);
}
@@ -109,7 +125,10 @@ mod tests {
let test_text = "Test string";
let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
assert_str_eq!(horizontally_scrollable_text.text, test_text);
}
@@ -122,14 +141,14 @@ mod tests {
let horizontally_scrollable_text = HorizontallyScrollableText {
text: test_text.to_owned(),
offset: RefCell::new(test_text.len() - 1),
offset: AtomicUsize::new(test_text.len() - 1),
};
assert_str_eq!(horizontally_scrollable_text.to_string(), "g");
let horizontally_scrollable_text = HorizontallyScrollableText {
text: test_text.to_owned(),
offset: RefCell::new(test_text.len()),
offset: AtomicUsize::new(test_text.len()),
};
assert!(horizontally_scrollable_text.to_string().is_empty());
@@ -140,7 +159,10 @@ mod tests {
let test_text = "Test string";
let horizontally_scrollable_text = HorizontallyScrollableText::new(test_text.to_owned());
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
assert_str_eq!(horizontally_scrollable_text.text, test_text);
}
@@ -158,18 +180,24 @@ mod tests {
fn test_horizontally_scrollable_text_scroll_text_left() {
let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
for i in 1..horizontally_scrollable_text.text.len() - 1 {
horizontally_scrollable_text.scroll_left();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), i);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
i
);
}
horizontally_scrollable_text.scroll_left();
assert_eq!(
*horizontally_scrollable_text.offset.borrow(),
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
horizontally_scrollable_text.text.len() - 1
);
}
@@ -180,37 +208,51 @@ mod tests {
horizontally_scrollable_text.scroll_left();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
1
);
assert_str_eq!(horizontally_scrollable_text.to_string(), "");
horizontally_scrollable_text.scroll_left();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 2);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
2
);
assert_str_eq!(horizontally_scrollable_text.to_string(), "");
horizontally_scrollable_text.scroll_left();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 2);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
2
);
assert!(horizontally_scrollable_text.to_string().is_empty());
}
#[test]
fn test_horizontally_scrollable_text_scroll_text_right() {
let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string");
*horizontally_scrollable_text.offset.borrow_mut() = horizontally_scrollable_text.text.len();
horizontally_scrollable_text
.offset
.store(horizontally_scrollable_text.len(), Ordering::SeqCst);
for i in 1..horizontally_scrollable_text.text.len() {
horizontally_scrollable_text.scroll_right();
assert_eq!(
*horizontally_scrollable_text.offset.borrow(),
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
horizontally_scrollable_text.text.len() - i
);
}
horizontally_scrollable_text.scroll_right();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
}
#[test]
@@ -220,7 +262,7 @@ mod tests {
horizontally_scrollable_text.scroll_home();
assert_eq!(
*horizontally_scrollable_text.offset.borrow(),
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
horizontally_scrollable_text.text.len()
);
}
@@ -231,19 +273,25 @@ mod tests {
horizontally_scrollable_text.scroll_home();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 2);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
2
);
}
#[test]
fn test_horizontally_scrollable_text_reset_offset() {
let horizontally_scrollable_text = HorizontallyScrollableText {
text: "Test string".to_owned(),
offset: RefCell::new(1),
offset: AtomicUsize::new(1),
};
horizontally_scrollable_text.reset_offset();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
}
#[test]
@@ -254,23 +302,38 @@ mod tests {
horizontally_scrollable_text.scroll_left_or_reset(width, true, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
1
);
horizontally_scrollable_text.scroll_left_or_reset(width, false, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
horizontally_scrollable_text.scroll_left_or_reset(width, true, false);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
horizontally_scrollable_text.scroll_left_or_reset(width, true, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
1
);
horizontally_scrollable_text.scroll_left_or_reset(test_text.len(), false, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
}
#[test]
@@ -278,11 +341,17 @@ mod tests {
let horizontally_scrollable_test = HorizontallyScrollableText::from("Test string");
horizontally_scrollable_test.scroll_left();
assert_eq!(*horizontally_scrollable_test.offset.borrow(), 1);
assert_eq!(
horizontally_scrollable_test.offset.load(Ordering::SeqCst),
1
);
horizontally_scrollable_test.scroll_left_or_reset(3, false, false);
assert_eq!(*horizontally_scrollable_test.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_test.offset.load(Ordering::SeqCst),
0
);
}
#[test]
@@ -292,15 +361,24 @@ mod tests {
horizontally_scrollable_text.scroll_left_or_reset(width, true, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
1
);
horizontally_scrollable_text.scroll_left_or_reset(width, true, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 2);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
2
);
horizontally_scrollable_text.scroll_left_or_reset(width, true, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
}
#[test]
@@ -310,32 +388,47 @@ mod tests {
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "Test sTrin우g");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
horizontally_scrollable_text.scroll_left();
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "Test sTring");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
1
);
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "Test sTrin");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
horizontally_scrollable_text.scroll_home();
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "Test sTrin");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 10);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
10
);
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "est sTrin");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 9);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
9
);
}
#[test]
@@ -344,17 +437,26 @@ mod tests {
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
horizontally_scrollable_text.pop();
assert!(horizontally_scrollable_text.text.is_empty());
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
horizontally_scrollable_text.pop();
assert!(horizontally_scrollable_text.text.is_empty());
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
}
#[test]
@@ -364,20 +466,29 @@ mod tests {
horizontally_scrollable_text.push('h');
assert_str_eq!(horizontally_scrollable_text.text, "Test stri우ngh");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
horizontally_scrollable_text.scroll_left();
horizontally_scrollable_text.push('l');
assert_str_eq!(horizontally_scrollable_text.text, "Test stri우nglh");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
1
);
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.push('리');
assert_str_eq!(horizontally_scrollable_text.text, "Test stri우nglh리");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_eq!(
horizontally_scrollable_text.offset.load(Ordering::SeqCst),
0
);
}
#[test]
+199 -43
View File
@@ -1,18 +1,21 @@
use std::fmt::{Display, Formatter};
use chrono::{DateTime, Utc};
use clap::ValueEnum;
use derivative::Derivative;
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use serde_json::{json, Number, Value};
use strum_macros::EnumIter;
use crate::models::HorizontallyScrollableText;
use crate::{models::HorizontallyScrollableText, serde_enum_from};
use super::Serdeable;
#[cfg(test)]
#[path = "radarr_models_tests.rs"]
mod radarr_models_tests;
#[derive(Default, Serialize, Debug)]
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddMovieBody {
pub tmdb_id: i64,
@@ -25,7 +28,7 @@ pub struct AddMovieBody {
pub add_options: AddOptions,
}
#[derive(Derivative, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddMovieSearchResult {
#[serde(deserialize_with = "super::from_i64")]
@@ -42,7 +45,7 @@ pub struct AddMovieSearchResult {
pub ratings: RatingsList,
}
#[derive(Default, Serialize, Debug, PartialEq, Eq)]
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddOptions {
pub monitor: String,
@@ -54,12 +57,12 @@ pub struct AddRootFolderBody {
pub path: String,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct BlocklistResponse {
pub records: Vec<BlocklistItem>,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BlocklistItem {
#[serde(deserialize_with = "super::from_i64")]
@@ -77,12 +80,12 @@ pub struct BlocklistItem {
pub movie: BlocklistItemMovie,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct BlocklistItemMovie {
pub title: HorizontallyScrollableText,
}
#[derive(Deserialize, Derivative, Default, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Derivative, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Collection {
#[serde(deserialize_with = "super::from_i64")]
@@ -99,7 +102,7 @@ pub struct Collection {
pub movies: Option<Vec<CollectionMovie>>,
}
#[derive(Derivative, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CollectionMovie {
pub title: HorizontallyScrollableText,
@@ -120,7 +123,7 @@ pub struct CommandBody {
pub name: String,
}
#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Credit {
pub person_name: String,
@@ -131,7 +134,7 @@ pub struct Credit {
pub credit_type: CreditType,
}
#[derive(Deserialize, Default, PartialEq, Eq, Clone, Debug)]
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum CreditType {
#[default]
@@ -139,7 +142,15 @@ pub enum CreditType {
Crew,
}
#[derive(Deserialize, Debug, Clone, Eq, PartialEq)]
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub struct DeleteMovieParams {
pub id: i64,
pub delete_movie_files: bool,
pub add_list_exclusion: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DiskSpace {
#[serde(deserialize_with = "super::from_i64")]
@@ -148,7 +159,7 @@ pub struct DiskSpace {
pub total_space: i64,
}
#[derive(Derivative, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DownloadRecord {
pub title: String,
@@ -167,12 +178,51 @@ pub struct DownloadRecord {
pub download_client: String,
}
#[derive(Default, Deserialize, Debug)]
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DownloadsResponse {
pub records: Vec<DownloadRecord>,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditCollectionParams {
pub collection_id: i64,
pub monitored: Option<bool>,
pub minimum_availability: Option<MinimumAvailability>,
pub quality_profile_id: Option<i64>,
pub root_folder_path: Option<String>,
pub search_on_add: Option<bool>,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditIndexerParams {
pub indexer_id: i64,
pub name: Option<String>,
pub enable_rss: Option<bool>,
pub enable_automatic_search: Option<bool>,
pub enable_interactive_search: Option<bool>,
pub url: Option<String>,
pub api_key: Option<String>,
pub seed_ratio: Option<String>,
pub tags: Option<Vec<i64>>,
pub priority: Option<i64>,
pub clear_tags: bool,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditMovieParams {
pub movie_id: i64,
pub monitored: Option<bool>,
pub minimum_availability: Option<MinimumAvailability>,
pub quality_profile_id: Option<i64>,
pub root_folder_path: Option<String>,
pub tags: Option<Vec<i64>>,
pub clear_tags: bool,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Indexer {
@@ -223,7 +273,7 @@ pub struct IndexerSettings {
pub whitelisted_hardcoded_subs: HorizontallyScrollableText,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct IndexerTestResult {
#[serde(deserialize_with = "super::from_i64")]
@@ -232,7 +282,7 @@ pub struct IndexerTestResult {
pub validation_failures: Vec<IndexerValidationFailure>,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct IndexerValidationFailure {
pub property_name: String,
@@ -240,12 +290,12 @@ pub struct IndexerValidationFailure {
pub severity: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Language {
pub name: String,
}
#[derive(Default, Deserialize, Clone, Debug, Eq, PartialEq)]
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Log {
pub time: DateTime<Utc>,
@@ -257,12 +307,12 @@ pub struct Log {
pub method: Option<String>,
}
#[derive(Default, Deserialize, Debug, Eq, PartialEq)]
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct LogResponse {
pub records: Vec<Log>,
}
#[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct MediaInfo {
@@ -286,7 +336,9 @@ pub struct MediaInfo {
pub scan_type: String,
}
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)]
#[derive(
Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum,
)]
#[serde(rename_all = "camelCase")]
pub enum MinimumAvailability {
#[default]
@@ -319,7 +371,7 @@ impl MinimumAvailability {
}
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum)]
pub enum Monitor {
#[default]
MovieOnly,
@@ -348,7 +400,7 @@ impl Monitor {
}
}
#[derive(Derivative, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Movie {
#[serde(deserialize_with = "super::from_i64")]
@@ -380,7 +432,7 @@ pub struct Movie {
pub collection: Option<MovieCollection>,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MovieCollection {
pub title: Option<String>,
@@ -393,7 +445,7 @@ pub struct MovieCommandBody {
pub movie_ids: Vec<i64>,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MovieFile {
pub relative_path: String,
@@ -402,7 +454,7 @@ pub struct MovieFile {
pub media_info: Option<MediaInfo>,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MovieHistoryItem {
pub source_title: HorizontallyScrollableText,
@@ -412,24 +464,33 @@ pub struct MovieHistoryItem {
pub event_type: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Quality {
pub name: String,
}
#[derive(Default, Deserialize, Debug)]
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct QualityProfile {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
pub name: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
impl From<(&i64, &String)> for QualityProfile {
fn from(value: (&i64, &String)) -> Self {
QualityProfile {
id: *value.0,
name: value.1.clone(),
}
}
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct QualityWrapper {
pub quality: Quality,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct QueueEvent {
pub trigger: String,
@@ -442,14 +503,14 @@ pub struct QueueEvent {
pub duration: Option<String>,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
pub struct Rating {
#[derivative(Default(value = "Number::from(0)"))]
pub value: Number,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RatingsList {
pub imdb: Option<Rating>,
@@ -457,7 +518,7 @@ pub struct RatingsList {
pub rotten_tomatoes: Option<Rating>,
}
#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct Release {
@@ -479,7 +540,7 @@ pub struct Release {
pub quality: QualityWrapper,
}
#[derive(Default, Serialize, Debug)]
#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseDownloadBody {
pub guid: String,
@@ -487,7 +548,7 @@ pub struct ReleaseDownloadBody {
pub movie_id: i64,
}
#[derive(Default, Deserialize, Debug, Clone, Eq, PartialEq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RootFolder {
#[serde(deserialize_with = "super::from_i64")]
@@ -499,25 +560,25 @@ pub struct RootFolder {
pub unmapped_folders: Option<Vec<UnmappedFolder>>,
}
#[derive(Deserialize, Debug)]
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SystemStatus {
pub version: String,
pub start_time: DateTime<Utc>,
}
#[derive(Default, Deserialize, Debug)]
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Tag {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
pub label: String,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Task {
pub name: String,
pub task_name: String,
pub task_name: TaskName,
#[serde(deserialize_with = "super::from_i64")]
pub interval: i64,
pub last_execution: DateTime<Utc>,
@@ -525,13 +586,39 @@ pub struct Task {
pub next_execution: DateTime<Utc>,
}
#[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)]
#[serde(rename_all = "PascalCase")]
pub enum TaskName {
#[default]
ApplicationCheckUpdate,
Backup,
CheckHealth,
CleanUpRecycleBin,
Housekeeping,
ImportListSync,
MessagingCleanup,
RefreshCollections,
RefreshMonitoredDownloads,
RefreshMovie,
RssSync,
}
impl Display for TaskName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let task_name = serde_json::to_string(&self)
.expect("Unable to serialize task name")
.replace('"', "");
write!(f, "{task_name}")
}
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
pub struct UnmappedFolder {
pub name: String,
pub path: String,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Update {
pub version: String,
@@ -542,9 +629,78 @@ pub struct Update {
pub changes: UpdateChanges,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UpdateChanges {
pub new: Option<Vec<String>>,
pub fixed: Option<Vec<String>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum RadarrSerdeable {
Value(Value),
Tag(Tag),
BlocklistResponse(BlocklistResponse),
Collections(Vec<Collection>),
Credits(Vec<Credit>),
DiskSpaces(Vec<DiskSpace>),
DownloadsResponse(DownloadsResponse),
Indexers(Vec<Indexer>),
IndexerSettings(IndexerSettings),
LogResponse(LogResponse),
Movie(Movie),
MovieHistoryItems(Vec<MovieHistoryItem>),
Movies(Vec<Movie>),
QualityProfiles(Vec<QualityProfile>),
QueueEvents(Vec<QueueEvent>),
Releases(Vec<Release>),
RootFolders(Vec<RootFolder>),
SystemStatus(SystemStatus),
Tags(Vec<Tag>),
Tasks(Vec<Task>),
Updates(Vec<Update>),
AddMovieSearchResults(Vec<AddMovieSearchResult>),
IndexerTestResults(Vec<IndexerTestResult>),
}
impl From<RadarrSerdeable> for Serdeable {
fn from(value: RadarrSerdeable) -> Serdeable {
Serdeable::Radarr(value)
}
}
impl From<()> for RadarrSerdeable {
fn from(_: ()) -> Self {
RadarrSerdeable::Value(json!({}))
}
}
serde_enum_from!(
RadarrSerdeable {
Value(Value),
Tag(Tag),
BlocklistResponse(BlocklistResponse),
Collections(Vec<Collection>),
Credits(Vec<Credit>),
DiskSpaces(Vec<DiskSpace>),
DownloadsResponse(DownloadsResponse),
Indexers(Vec<Indexer>),
IndexerSettings(IndexerSettings),
LogResponse(LogResponse),
Movie(Movie),
MovieHistoryItems(Vec<MovieHistoryItem>),
Movies(Vec<Movie>),
QualityProfiles(Vec<QualityProfile>),
QueueEvents(Vec<QueueEvent>),
Releases(Vec<Release>),
RootFolders(Vec<RootFolder>),
SystemStatus(SystemStatus),
Tags(Vec<Tag>),
Tasks(Vec<Task>),
Updates(Vec<Update>),
AddMovieSearchResults(Vec<AddMovieSearchResult>),
IndexerTestResults(Vec<IndexerTestResult>),
}
);
+337 -1
View File
@@ -1,8 +1,25 @@
#[cfg(test)]
mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use serde_json::json;
use crate::models::radarr_models::{DownloadRecord, MinimumAvailability, Monitor};
use crate::models::{
radarr_models::{
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, DiskSpace,
DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult, Log,
LogResponse, MinimumAvailability, Monitor, Movie, MovieHistoryItem, QualityProfile,
QueueEvent, RadarrSerdeable, Release, RootFolder, SystemStatus, Tag, Task, TaskName, Update,
},
Serdeable,
};
#[test]
fn test_task_name_display() {
assert_str_eq!(
TaskName::ApplicationCheckUpdate.to_string(),
"ApplicationCheckUpdate"
);
}
#[test]
fn test_minimum_availability_display() {
@@ -70,4 +87,323 @@ mod tests {
assert_eq!(result, expected_record);
}
#[test]
fn test_radarr_serdeable_from() {
let radarr_serdeable = RadarrSerdeable::Value(json!({}));
let serdeable: Serdeable = Serdeable::from(radarr_serdeable.clone());
assert_eq!(serdeable, Serdeable::Radarr(radarr_serdeable));
}
#[test]
fn test_radarr_serdeable_from_unit() {
let radarr_serdeable = RadarrSerdeable::from(());
assert_eq!(radarr_serdeable, RadarrSerdeable::Value(json!({})));
}
#[test]
fn test_radarr_serdeable_from_value() {
let value = json!({"test": "test"});
let radarr_serdeable: RadarrSerdeable = value.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Value(value));
}
#[test]
fn test_radarr_serdeable_from_tag() {
let tag = Tag {
id: 1,
..Tag::default()
};
let radarr_serdeable: RadarrSerdeable = tag.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Tag(tag));
}
#[test]
fn test_radarr_serdeable_from_blocklist_response() {
let blocklist_response = BlocklistResponse {
records: vec![BlocklistItem {
id: 1,
..BlocklistItem::default()
}],
};
let radarr_serdeable: RadarrSerdeable = blocklist_response.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::BlocklistResponse(blocklist_response)
);
}
#[test]
fn test_radarr_serdeable_from_collections() {
let collections = vec![Collection {
id: 1,
..Collection::default()
}];
let radarr_serdeable: RadarrSerdeable = collections.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Collections(collections));
}
#[test]
fn test_radarr_serdeable_from_credits() {
let credits = vec![Credit {
person_name: "me".to_owned(),
..Credit::default()
}];
let radarr_serdeable: RadarrSerdeable = credits.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Credits(credits));
}
#[test]
fn test_radarr_serdeable_from_disk_spaces() {
let disk_spaces = vec![DiskSpace {
free_space: 1,
total_space: 1,
}];
let radarr_serdeable: RadarrSerdeable = disk_spaces.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::DiskSpaces(disk_spaces));
}
#[test]
fn test_radarr_serdeable_from_downloads_response() {
let downloads_response = DownloadsResponse {
records: vec![DownloadRecord {
id: 1,
..DownloadRecord::default()
}],
};
let radarr_serdeable: RadarrSerdeable = downloads_response.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::DownloadsResponse(downloads_response)
);
}
#[test]
fn test_radarr_serdeable_from_indexers() {
let indexers = vec![Indexer {
id: 1,
..Indexer::default()
}];
let radarr_serdeable: RadarrSerdeable = indexers.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Indexers(indexers));
}
#[test]
fn test_radarr_serdeable_from_indexer_settings() {
let indexer_settings = IndexerSettings {
id: 1,
..IndexerSettings::default()
};
let radarr_serdeable: RadarrSerdeable = indexer_settings.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::IndexerSettings(indexer_settings)
);
}
#[test]
fn test_radarr_serdeable_from_log_response() {
let log_response = LogResponse {
records: vec![Log {
level: "info".to_owned(),
..Log::default()
}],
};
let radarr_serdeable: RadarrSerdeable = log_response.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::LogResponse(log_response));
}
#[test]
fn test_radarr_serdeable_from_movie() {
let movie = Movie {
id: 1,
..Movie::default()
};
let radarr_serdeable: RadarrSerdeable = movie.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Movie(movie));
}
#[test]
fn test_radarr_serdeable_from_movie_history_items() {
let movie_history_items = vec![MovieHistoryItem {
event_type: "test".to_owned(),
..MovieHistoryItem::default()
}];
let radarr_serdeable: RadarrSerdeable = movie_history_items.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::MovieHistoryItems(movie_history_items)
);
}
#[test]
fn test_radarr_serdeable_from_movies() {
let movies = vec![Movie {
id: 1,
..Movie::default()
}];
let radarr_serdeable: RadarrSerdeable = movies.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Movies(movies));
}
#[test]
fn test_radarr_serdeable_from_quality_profiles() {
let quality_profiles = vec![QualityProfile {
id: 1,
..QualityProfile::default()
}];
let radarr_serdeable: RadarrSerdeable = quality_profiles.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::QualityProfiles(quality_profiles)
);
}
#[test]
fn test_radarr_serdeable_from_queue_events() {
let queue_events = vec![QueueEvent {
trigger: "test".to_owned(),
..QueueEvent::default()
}];
let radarr_serdeable: RadarrSerdeable = queue_events.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::QueueEvents(queue_events));
}
#[test]
fn test_radarr_serdeable_from_releases() {
let releases = vec![Release {
size: 1,
..Release::default()
}];
let radarr_serdeable: RadarrSerdeable = releases.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Releases(releases));
}
#[test]
fn test_radarr_serdeable_from_root_folders() {
let root_folders = vec![RootFolder {
id: 1,
..RootFolder::default()
}];
let radarr_serdeable: RadarrSerdeable = root_folders.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::RootFolders(root_folders));
}
#[test]
fn test_radarr_serdeable_from_system_status() {
let system_status = SystemStatus {
version: "1".to_owned(),
..SystemStatus::default()
};
let radarr_serdeable: RadarrSerdeable = system_status.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::SystemStatus(system_status)
);
}
#[test]
fn test_radarr_serdeable_from_tags() {
let tags = vec![Tag {
id: 1,
..Tag::default()
}];
let radarr_serdeable: RadarrSerdeable = tags.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Tags(tags));
}
#[test]
fn test_radarr_serdeable_from_tasks() {
let tasks = vec![Task {
name: "test".to_owned(),
..Task::default()
}];
let radarr_serdeable: RadarrSerdeable = tasks.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Tasks(tasks));
}
#[test]
fn test_radarr_serdeable_from_updates() {
let updates = vec![Update {
version: "test".to_owned(),
..Update::default()
}];
let radarr_serdeable: RadarrSerdeable = updates.clone().into();
assert_eq!(radarr_serdeable, RadarrSerdeable::Updates(updates));
}
#[test]
fn test_radarr_serdeable_from_add_movie_search_results() {
let add_movie_search_results = vec![AddMovieSearchResult {
tmdb_id: 1,
..AddMovieSearchResult::default()
}];
let radarr_serdeable: RadarrSerdeable = add_movie_search_results.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::AddMovieSearchResults(add_movie_search_results)
);
}
#[test]
fn test_radarr_serdeable_from_indexer_test_results() {
let indexer_test_results = vec![IndexerTestResult {
id: 1,
..IndexerTestResult::default()
}];
let radarr_serdeable: RadarrSerdeable = indexer_test_results.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::IndexerTestResults(indexer_test_results)
);
}
}
@@ -86,7 +86,7 @@ impl<'a> Default for RadarrData<'a> {
RadarrData {
root_folders: StatefulTable::default(),
disk_space_vec: Vec::new(),
version: String::default(),
version: String::new(),
start_time: DateTime::default(),
movies: StatefulTable::default(),
selected_block: BlockSelectionState::default(),
@@ -269,7 +269,7 @@ pub enum ActiveRadarrBlock {
FilterMovies,
FilterMoviesError,
Indexers,
IndexerSettingsPrompt,
AllIndexerSettingsPrompt,
IndexerSettingsAvailabilityDelayInput,
IndexerSettingsConfirmPrompt,
IndexerSettingsMaximumSizeInput,
@@ -466,7 +466,7 @@ pub static EDIT_INDEXER_NZB_SELECTION_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::EditIndexerConfirmPrompt,
];
pub static INDEXER_SETTINGS_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::IndexerSettingsPrompt,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
ActiveRadarrBlock::IndexerSettingsConfirmPrompt,
ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
@@ -423,7 +423,7 @@ mod tests {
#[test]
fn test_indexer_settings_blocks_contents() {
assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 10);
assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveRadarrBlock::IndexerSettingsPrompt));
assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveRadarrBlock::AllIndexerSettingsPrompt));
assert!(
INDEXER_SETTINGS_BLOCKS.contains(&ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput)
);
+41 -16
View File
@@ -1,7 +1,8 @@
use std::fmt::Debug;
use std::sync::Arc;
use anyhow::anyhow;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use log::{debug, error, warn};
use regex::Regex;
use reqwest::{Client, RequestBuilder};
@@ -13,7 +14,10 @@ use tokio::sync::{Mutex, MutexGuard};
use tokio_util::sync::CancellationToken;
use crate::app::App;
use crate::models::Serdeable;
use crate::network::radarr_network::RadarrEvent;
#[cfg(test)]
use mockall::automock;
pub mod radarr_network;
mod utils;
@@ -22,17 +26,41 @@ mod utils;
#[path = "network_tests.rs"]
mod network_tests;
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum NetworkEvent {
Radarr(RadarrEvent),
}
#[cfg_attr(test, automock)]
#[async_trait]
pub trait NetworkTrait {
async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result<Serdeable>;
}
#[derive(Clone)]
pub struct Network<'a, 'b> {
client: Client,
cancellation_token: CancellationToken,
pub app: &'a Arc<Mutex<App<'b>>>,
}
#[async_trait]
impl<'a, 'b> NetworkTrait for Network<'a, 'b> {
async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result<Serdeable> {
let resp = match network_event {
NetworkEvent::Radarr(radarr_event) => self
.handle_radarr_event(radarr_event)
.await
.map(Serdeable::from),
};
let mut app = self.app.lock().await;
app.is_loading = false;
resp
}
}
impl<'a, 'b> Network<'a, 'b> {
pub fn new(app: &'a Arc<Mutex<App<'b>>>, cancellation_token: CancellationToken) -> Self {
Network {
@@ -42,22 +70,14 @@ impl<'a, 'b> Network<'a, 'b> {
}
}
pub async fn handle_network_event(&mut self, network_event: NetworkEvent) {
match network_event {
NetworkEvent::Radarr(radarr_event) => self.handle_radarr_event(radarr_event).await,
}
let mut app = self.app.lock().await;
app.is_loading = false;
}
pub async fn handle_request<B, R>(
async fn handle_request<B, R>(
&mut self,
request_props: RequestProps<B>,
mut app_update_fn: impl FnMut(R, MutexGuard<'_, App<'_>>),
) where
) -> Result<R>
where
B: Serialize + Default + Debug,
R: DeserializeOwned,
R: DeserializeOwned + Default + Clone,
{
let ignore_status_code = request_props.ignore_status_code;
let method = request_props.method;
@@ -68,6 +88,7 @@ impl<'a, 'b> Network<'a, 'b> {
let mut app = self.app.lock().await;
self.cancellation_token = app.reset_cancellation_token();
app.is_loading = false;
Ok(R::default())
}
resp = self.call_api(request_props).await.send() => {
match resp {
@@ -78,7 +99,8 @@ impl<'a, 'b> Network<'a, 'b> {
match utils::parse_response::<R>(response).await {
Ok(value) => {
let app = self.app.lock().await;
app_update_fn(value, app);
app_update_fn(value.clone(), app);
Ok(value)
}
Err(e) => {
error!("Failed to parse response! {e:?}");
@@ -87,10 +109,11 @@ impl<'a, 'b> Network<'a, 'b> {
.lock()
.await
.handle_error(anyhow!("Failed to parse response! {e:?}"));
Err(anyhow!("Failed to parse response! {e:?}"))
}
}
}
RequestMethod::Delete | RequestMethod::Put => (),
RequestMethod::Delete | RequestMethod::Put => Ok(R::default()),
}
} else {
let status = response.status();
@@ -102,6 +125,7 @@ impl<'a, 'b> Network<'a, 'b> {
error!("Request failed. Received {status} response code with body: {response_body}");
self.app.lock().await.handle_error(anyhow!("Request failed. Received {status} response code with body: {error_body}"));
Err(anyhow!("Request failed. Received {status} response code with body: {error_body}"))
}
}
Err(e) => {
@@ -111,6 +135,7 @@ impl<'a, 'b> Network<'a, 'b> {
.lock()
.await
.handle_error(anyhow!("Failed to send request. {e} "));
Err(anyhow!("Failed to send request. {e} "))
}
}
}
+50 -13
View File
@@ -14,7 +14,7 @@ mod tests {
use crate::app::{App, AppConfig, RadarrConfig};
use crate::models::HorizontallyScrollableText;
use crate::network::radarr_network::RadarrEvent;
use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps};
use crate::network::{Network, NetworkEvent, NetworkTrait, RequestMethod, RequestProps};
#[tokio::test]
async fn test_handle_network_event_radarr_event() {
@@ -22,6 +22,7 @@ mod tests {
let radarr_server = server
.mock("GET", "/api/v3/health")
.with_status(200)
.with_body("{}")
.create_async()
.await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned();
@@ -41,7 +42,7 @@ mod tests {
let app_arc = Arc::new(Mutex::new(app));
let mut network = Network::new(&app_arc, CancellationToken::new());
network
let _ = network
.handle_network_event(RadarrEvent::HealthCheck.into())
.await;
@@ -65,7 +66,7 @@ mod tests {
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
network
let _ = network
.handle_request::<Test, ()>(
RequestProps {
uri: format!("{}/test", server.url()),
@@ -91,7 +92,7 @@ mod tests {
let (async_server, app_arc, server) = mock_api(request_method, 200, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
network
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
@@ -106,6 +107,13 @@ mod tests {
async_server.assert_async().await;
assert_str_eq!(app_arc.lock().await.error.text, "Test");
assert!(resp.is_ok());
assert_eq!(
resp.unwrap(),
Test {
value: "Test".to_owned()
}
);
}
#[rstest]
@@ -115,9 +123,9 @@ mod tests {
) {
let (async_server, app_arc, server) = mock_api(request_method, 400, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut test_result = String::default();
let mut test_result = String::new();
network
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
@@ -132,6 +140,13 @@ mod tests {
async_server.assert_async().await;
assert!(app_arc.lock().await.error.text.is_empty());
assert!(resp.is_ok());
assert_eq!(
resp.unwrap(),
Test {
value: "Test".to_owned()
}
);
}
#[tokio::test]
@@ -148,7 +163,7 @@ mod tests {
let mut network = Network::new(&app_arc, cancellation_token);
network.cancellation_token.cancel();
network
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
@@ -164,6 +179,8 @@ mod tests {
assert!(!async_server.matched_async().await);
assert!(app_arc.lock().await.error.text.is_empty());
assert!(!network.cancellation_token.is_cancelled());
assert!(resp.is_ok());
assert_eq!(resp.unwrap(), Test::default());
}
#[tokio::test]
@@ -179,7 +196,7 @@ mod tests {
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
network
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
@@ -199,6 +216,11 @@ mod tests {
.error
.text
.starts_with("Failed to parse response!"));
assert!(resp.is_err());
assert!(resp
.unwrap_err()
.to_string()
.starts_with("Failed to parse response!"));
}
#[tokio::test]
@@ -206,10 +228,10 @@ mod tests {
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
network
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: String::default(),
uri: String::new(),
method: RequestMethod::Get,
body: None,
api_token: "test1234".to_owned(),
@@ -225,6 +247,11 @@ mod tests {
.error
.text
.starts_with("Failed to send request."));
assert!(resp.is_err());
assert!(resp
.unwrap_err()
.to_string()
.starts_with("Failed to send request."));
}
#[rstest]
@@ -241,7 +268,7 @@ mod tests {
let (async_server, app_arc, server) = mock_api(request_method, 404, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
network
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
@@ -259,6 +286,11 @@ mod tests {
app_arc.lock().await.error.text,
r#"Request failed. Received 404 Not Found response code with body: { "value": "Test" }"#
);
assert!(resp.is_err());
assert_str_eq!(
resp.unwrap_err().to_string(),
r#"Request failed. Received 404 Not Found response code with body: { "value": "Test" }"#
);
}
#[tokio::test]
@@ -266,7 +298,7 @@ mod tests {
let (async_server, app_arc, server) = mock_api(RequestMethod::Post, 404, false).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
network
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
@@ -284,6 +316,11 @@ mod tests {
app_arc.lock().await.error.text,
r#"Request failed. Received 404 Not Found response code with body: "#
);
assert!(resp.is_err());
assert_str_eq!(
resp.unwrap_err().to_string(),
r#"Request failed. Received 404 Not Found response code with body: "#
);
}
#[rstest]
@@ -335,7 +372,7 @@ mod tests {
async_server.assert_async().await;
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
struct Test {
pub value: String,
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -1,3 +1,5 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text};
@@ -187,7 +189,7 @@ pub fn draw_input_box_popup(
.areas(area);
let input_box = InputBox::new(&box_content.text)
.offset(*box_content.offset.borrow())
.offset(box_content.offset.load(Ordering::SeqCst))
.block(title_block_centered(box_title));
input_box.show_cursor(f, text_box_area);
@@ -1,3 +1,5 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::ListItem;
use ratatui::Frame;
@@ -142,7 +144,7 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
let root_folder_input_box = InputBox::new(&path.text)
.offset(*path.offset.borrow())
.offset(path.offset.load(Ordering::SeqCst))
.label("Root Folder")
.highlighted(selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput);
+8 -6
View File
@@ -1,3 +1,5 @@
use std::sync::atomic::Ordering;
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::Route;
@@ -79,22 +81,22 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
let name_input_box = InputBox::new(&edit_indexer_modal.name.text)
.offset(*edit_indexer_modal.name.offset.borrow())
.offset(edit_indexer_modal.name.offset.load(Ordering::SeqCst))
.label("Name")
.highlighted(selected_block == &ActiveRadarrBlock::EditIndexerNameInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditIndexerNameInput);
let url_input_box = InputBox::new(&edit_indexer_modal.url.text)
.offset(*edit_indexer_modal.url.offset.borrow())
.offset(edit_indexer_modal.url.offset.load(Ordering::SeqCst))
.label("URL")
.highlighted(selected_block == &ActiveRadarrBlock::EditIndexerUrlInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditIndexerUrlInput);
let api_key_input_box = InputBox::new(&edit_indexer_modal.api_key.text)
.offset(*edit_indexer_modal.api_key.offset.borrow())
.offset(edit_indexer_modal.api_key.offset.load(Ordering::SeqCst))
.label("API Key")
.highlighted(selected_block == &ActiveRadarrBlock::EditIndexerApiKeyInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditIndexerApiKeyInput);
let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text)
.offset(*edit_indexer_modal.tags.offset.borrow())
.offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == &ActiveRadarrBlock::EditIndexerTagsInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput);
@@ -105,12 +107,12 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if protocol == "torrent" {
let seed_ratio_input_box = InputBox::new(&edit_indexer_modal.seed_ratio.text)
.offset(*edit_indexer_modal.seed_ratio.offset.borrow())
.offset(edit_indexer_modal.seed_ratio.offset.load(Ordering::SeqCst))
.label("Seed Ratio")
.highlighted(selected_block == &ActiveRadarrBlock::EditIndexerSeedRatioInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditIndexerSeedRatioInput);
let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text)
.offset(*edit_indexer_modal.tags.offset.borrow())
.offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == &ActiveRadarrBlock::EditIndexerTagsInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput);
@@ -1,3 +1,5 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::Frame;
@@ -114,7 +116,12 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
.selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput);
let whitelisted_subs_input_box =
InputBox::new(&indexer_settings.whitelisted_hardcoded_subs.text)
.offset(*indexer_settings.whitelisted_hardcoded_subs.offset.borrow())
.offset(
indexer_settings
.whitelisted_hardcoded_subs
.offset
.load(Ordering::SeqCst),
)
.label("Whitelisted Subtitle Tags")
.highlighted(
selected_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
+5 -3
View File
@@ -1,3 +1,5 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
@@ -131,14 +133,14 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.margin(1)
.areas(area);
let block_content = &app.data.radarr_data.add_movie_search.as_ref().unwrap().text;
let offset = *app
let offset = app
.data
.radarr_data
.add_movie_search
.as_ref()
.unwrap()
.offset
.borrow();
.load(Ordering::SeqCst);
let search_results_row_mapping = |movie: &AddMovieSearchResult| {
let (hours, minutes) = convert_runtime(movie.runtime);
let imdb_rating = movie
@@ -416,7 +418,7 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
let tags_input_box = InputBox::new(&tags.text)
.offset(*tags.offset.borrow())
.offset(tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == &ActiveRadarrBlock::AddMovieTagsInput)
.selected(active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput);
+4 -2
View File
@@ -1,3 +1,5 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::Layout;
use ratatui::widgets::ListItem;
@@ -145,12 +147,12 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
let path_input_box = InputBox::new(&path.text)
.offset(*path.offset.borrow())
.offset(path.offset.load(Ordering::SeqCst))
.label("Path")
.highlighted(selected_block == &ActiveRadarrBlock::EditMoviePathInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditMoviePathInput);
let tags_input_box = InputBox::new(&tags.text)
.offset(*tags.offset.borrow())
.offset(tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == &ActiveRadarrBlock::EditMovieTagsInput)
.selected(active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput);