diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs index fe04b26..adeeafb 100644 --- a/src/cli/sonarr/edit_command_handler.rs +++ b/src/cli/sonarr/edit_command_handler.rs @@ -1,13 +1,14 @@ use std::sync::Arc; use anyhow::Result; -use clap::{ArgGroup, Subcommand}; +use clap::{ArgAction, ArgGroup, Subcommand}; use tokio::sync::Mutex; use crate::{ app::App, - cli::{CliCommandHandler, Command}, + cli::{mutex_flags_or_option, CliCommandHandler, Command}, models::{ + servarr_models::EditIndexerParams, sonarr_models::{IndexerSettings, SonarrSerdeable}, Serdeable, }, @@ -56,6 +57,97 @@ pub enum SonarrEditCommand { )] rss_sync_interval: Option, }, + #[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, + #[arg( + long, + help = "Indicate to Sonarr that this indexer should be used when Sonarr periodically looks for releases via RSS Sync", + conflicts_with = "disable_rss" + )] + enable_rss: bool, + #[arg( + long, + help = "Disable using this indexer when Sonarr periodically looks for releases via RSS Sync", + conflicts_with = "enable_rss" + )] + disable_rss: bool, + #[arg( + long, + help = "Indicate to Sonarr that this indexer should be used when automatic searches are performed via the UI or by Sonarr", + 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 Sonarr", + conflicts_with = "enable_automatic_search" + )] + disable_automatic_search: bool, + #[arg( + long, + help = "Indicate to Sonarr 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, + #[arg(long, help = "The API key used to access the indexer's API")] + api_key: Option, + #[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, + #[arg( + long, + help = "Only use this indexer for series with at least one matching tag ID. Leave blank to use with all series.", + value_parser, + action = ArgAction::Append, + conflicts_with = "clear_tags" + )] + tag: Option>, + #[arg( + long, + help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Sonarr will still use all enabled indexers for RSS Sync and Searching" + )] + priority: Option, + #[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")] + clear_tags: bool, + }, } impl From for Command { @@ -113,6 +205,47 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH String::new() } } + SonarrEditCommand::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, + }; + + self + .network + .handle_network_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)).into()) + .await?; + "Indexer updated".to_owned() + } }; Ok(result) diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs index e8bb470..352e373 100644 --- a/src/cli/sonarr/edit_command_handler_tests.rs +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -114,6 +114,250 @@ mod tests { assert_eq!(edit_command, expected_args); } } + + #[test] + fn test_edit_indexer_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "indexer"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_indexer_with_indexer_id_still_requires_arguments() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_indexer_rss_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-rss", + "--disable-rss", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_automatic_search_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-automatic-search", + "--disable-automatic-search", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_interactive_search_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-interactive-search", + "--disable-interactive-search", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_tag_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--tag", + "1", + "--clear-tags", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[rstest] + fn test_edit_indexer_assert_argument_flags_require_args( + #[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + flag, + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() { + let expected_args = SonarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: false, + disable_rss: false, + enable_automatic_search: false, + disable_automatic_search: false, + enable_interactive_search: false, + disable_interactive_search: false, + url: None, + api_key: None, + seed_ratio: None, + tag: None, + priority: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--name", + "Test", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_indexer_tag_argument_is_repeatable() { + let expected_args = SonarrEditCommand::Indexer { + indexer_id: 1, + name: None, + enable_rss: false, + disable_rss: false, + enable_automatic_search: false, + disable_automatic_search: false, + enable_interactive_search: false, + disable_interactive_search: false, + url: None, + api_key: None, + seed_ratio: None, + tag: Some(vec![1, 2]), + priority: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--tag", + "1", + "--tag", + "2", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_indexer_all_arguments_defined() { + let expected_args = SonarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: true, + disable_rss: false, + enable_automatic_search: true, + disable_automatic_search: false, + enable_interactive_search: true, + disable_interactive_search: false, + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tag: Some(vec![1, 2]), + priority: Some(25), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--name", + "Test", + "--enable-rss", + "--enable-automatic-search", + "--enable-interactive-search", + "--url", + "http://test.com", + "--api-key", + "testKey", + "--seed-ratio", + "1.2", + "--tag", + "1", + "--tag", + "2", + "--priority", + "25", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } } mod handler { @@ -130,6 +374,7 @@ mod tests { CliCommandHandler, }, models::{ + servarr_models::EditIndexerParams, sonarr_models::{IndexerSettings, SonarrSerdeable}, Serdeable, }, @@ -192,5 +437,58 @@ mod tests { assert!(result.is_ok()); } + + #[tokio::test] + async fn test_handle_edit_indexer_command() { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tags: Some(vec![1, 2]), + priority: Some(25), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_indexer_command = SonarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: true, + disable_rss: false, + enable_automatic_search: true, + disable_automatic_search: false, + enable_interactive_search: true, + disable_interactive_search: false, + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tag: Some(vec![1, 2]), + priority: Some(25), + clear_tags: false, + }; + + let result = + SonarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } } }