diff --git a/Cargo.lock b/Cargo.lock index b6c38a4..6a2f9e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" @@ -130,6 +130,33 @@ dependencies = [ "serde_json", ] +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -162,6 +189,9 @@ name = "bimap" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +dependencies = [ + "serde", +] [[package]] name = "bitflags" @@ -175,6 +205,17 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -214,6 +255,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -229,6 +276,55 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.5.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9646e2e245bf62f45d39a0f3f36f1171ad1ea0d6967fd114bca72cb02a8fcdfb" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "colorchoice" version = "1.0.1" @@ -328,6 +424,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] + [[package]] name = "deranged" version = "0.3.11" @@ -360,6 +466,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "directories" version = "5.0.1" @@ -381,6 +493,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "either" version = "1.13.0" @@ -448,6 +572,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures" version = "0.3.30" @@ -887,19 +1017,27 @@ dependencies = [ [[package]] name = "managarr" -version = "0.0.35" +version = "0.0.36" dependencies = [ "anyhow", + "assert_cmd", + "async-trait", "backtrace", "bimap", "chrono", + "clap", + "clap_complete", + "colored", "confy", "crossterm 0.27.0", + "ctrlc", "derivative", "human-panic", "indoc", + "itertools", "log", "log4rs", + "mockall", "mockito", "pretty_assertions", "ratatui", @@ -962,6 +1100,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "mockito" version = "1.4.0" @@ -998,6 +1162,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1172,6 +1348,33 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -1612,6 +1815,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.26.3" @@ -1695,6 +1904,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.63" @@ -1989,6 +2204,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.1" @@ -2129,6 +2353,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 293fc52..c1548e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,24 @@ [package] name = "managarr" -version = "0.0.35" +version = "0.0.36" authors = ["Alex Clarke "] -description = "A TUI to manage your Servarrs" +description = "A TUI and CLI to manage your Servarrs" keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"] documentation = "https://github.com/Dark-Alex-17/managarr" repository = "https://github.com/Dark-Alex-17/managarr" homepage = "https://github.com/Dark-Alex-17/managarr" readme = "README.md" edition = "2021" -rust-version = "1.76.0" +rust-version = "1.82.0" [dependencies] anyhow = "1.0.68" backtrace = "0.3.67" -bimap = "0.6.3" +bimap = { version = "0.6.3", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] } -confy = { version = "0.6.0", default-features = false, features = ["yaml_conf"] } +confy = { version = "0.6.0", default-features = false, features = [ + "yaml_conf", +] } crossterm = "0.27.0" derivative = "2.2.0" human-panic = "1.1.3" @@ -28,14 +30,22 @@ reqwest = { version = "0.11.14", features = ["json"] } serde_yaml = "0.9.16" serde_json = "1.0.91" serde = { version = "1.0", features = ["derive"] } -strum = {version = "0.26.1", features = ["derive"] } +strum = { version = "0.26.1", features = ["derive"] } strum_macros = "0.26.1" tokio = { version = "1.36.0", features = ["full"] } tokio-util = "0.7.8" ratatui = { version = "0.28.0", features = ["all-widgets"] } urlencoding = "2.1.2" +clap = { version = "4.5.20", features = ["derive", "cargo"] } +clap_complete = "4.5.33" +itertools = "0.13.0" +ctrlc = "3.4.5" +colored = "2.1.0" +async-trait = "0.1.83" [dev-dependencies] +assert_cmd = "2.0.16" +mockall = "0.13.0" mockito = "1.0.0" pretty_assertions = "1.3.0" rstest = "0.18.2" diff --git a/README.md b/README.md index c665f55..3f9cbf9 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,70 @@ curl https://raw.githubusercontent.com/Dark-Alex-17/managarr-demo/main/managarr- - [ ] Support for Tautulli +### The Managarr CLI +Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs. + +All management features available in the TUI are also available in the CLI. + +The CLI can be helpful for automating tasks or for use in scripts. For example, you can use the CLI to trigger a search for a movie, or to add a movie to your library. + +To see all available commands, simply run `managarr --help`: + +```shell +$ managarr --help +managarr 0.0.36 +Alex Clarke + +A TUI and CLI to manage your Servarrs + +Usage: managarr [COMMAND] + +Commands: + radarr Commands for manging your Radarr instance + completions Generate shell completions for the Managarr CLI + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version +``` + +All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Radarr, you would run: + +```shell +$ managarr radarr --help +Commands for manging your Radarr instance + +Usage: managarr radarr + +Commands: + add Commands to add or create new resources within your Radarr instance + delete Commands to delete resources from your Radarr instance + edit Commands to edit resources in your Radarr instance + get Commands to fetch details of the resources in your Radarr instance + list Commands to list attributes from your Radarr instance + refresh Commands to refresh the data in your Radarr instance + clear-blocklist Clear the blocklist + download-release Manually download the given release for the specified movie ID + manual-search Trigger a manual search of releases for the movie with the given ID + search-new-movie Search for a new film to add to Radarr + start-task Start the specified Radarr task + test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}' + test-all-indexers Test all indexers + trigger-automatic-search Trigger an automatic search for the movie with the specified ID + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help +``` + +**Usage Tip:** The CLI is even more powerful and useful when used in conjunction with the `jq` CLI tool. This allows you to parse the JSON response from the Managarr CLI and use it in your scripts; For example, to extract the `movieId` of the movie "Ad Astra", you would run: + +```shell +$ managarr radarr list movies | jq '.[] | select(.title == "Ad Astra") | .id' +277 +``` + ## Installation ### Docker diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 1c97a13..8fc0a0a 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -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; } diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index c43613e..4dbd9f1 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -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); diff --git a/src/cli/cli_tests.rs b/src/cli/cli_tests.rs new file mode 100644 index 0000000..c080b00 --- /dev/null +++ b/src/cli/cli_tests.rs @@ -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, + ) { + 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::(RadarrEvent::GetBlocklist.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse( + BlocklistResponse { + records: vec![BlocklistItem::default()], + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::(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()); + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..05854f0 --- /dev/null +++ b/src/cli/mod.rs @@ -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> { + fn with(app: &'a Arc>>, command: T, network: &'a mut dyn NetworkTrait) -> Self; + async fn handle(self) -> Result<()>; +} + +pub(crate) async fn handle_command( + app: &Arc>>, + 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 { + 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); + }; +} diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs new file mode 100644 index 0000000..ffd362c --- /dev/null +++ b/src/cli/radarr/add_command_handler.rs @@ -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, + #[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 for Command { + fn from(value: RadarrAddCommand) -> Self { + Command::Radarr(RadarrCommand::Add(value)) + } +} + +pub(super) struct RadarrAddCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: RadarrAddCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHandler<'a, 'b> { + fn with(_app: &'a Arc>>, 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(()) + } +} diff --git a/src/cli/radarr/add_command_handler_tests.rs b/src/cli/radarr/add_command_handler_tests.rs new file mode 100644 index 0000000..8974454 --- /dev/null +++ b/src/cli/radarr/add_command_handler_tests.rs @@ -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::( + 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::( + 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::( + 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()); + } + } +} diff --git a/src/cli/radarr/delete_command_handler.rs b/src/cli/radarr/delete_command_handler.rs new file mode 100644 index 0000000..db26775 --- /dev/null +++ b/src/cli/radarr/delete_command_handler.rs @@ -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 for Command { + fn from(value: RadarrDeleteCommand) -> Self { + Command::Radarr(RadarrCommand::Delete(value)) + } +} + +pub(super) struct RadarrDeleteCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: RadarrDeleteCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + 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(()) + } +} diff --git a/src/cli/radarr/delete_command_handler_tests.rs b/src/cli/radarr/delete_command_handler_tests.rs new file mode 100644 index 0000000..7e20597 --- /dev/null +++ b/src/cli/radarr/delete_command_handler_tests.rs @@ -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::( + 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::( + 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::( + 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::( + 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::( + 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::( + 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()); + } + } +} diff --git a/src/cli/radarr/edit_command_handler.rs b/src/cli/radarr/edit_command_handler.rs new file mode 100644 index 0000000..666828c --- /dev/null +++ b/src/cli/radarr/edit_command_handler.rs @@ -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, + #[arg( + long, + help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited" + )] + maximum_size: Option, + #[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, + #[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, + #[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, + #[arg( + long, + help = "A comma separated list of subtitle tags that will not be considered hardcoded" + )] + whitelisted_subtitle_tags: Option, + }, + #[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, + #[arg( + long, + help = "The ID of the quality profile that all movies in this collection should use" + )] + quality_profile_id: Option, + #[arg( + long, + help = "The root folder path that all movies in this collection should exist under" + )] + root_folder_path: Option, + #[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, + #[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, + #[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 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>, + #[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, + #[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, + #[arg(long, help = "The ID of the quality profile to use for this movie")] + quality_profile_id: Option, + #[arg( + long, + help = "The root folder path where all film data and metadata should live" + )] + root_folder_path: Option, + #[arg( + long, + help = "Tag IDs to tag this movie with", + value_parser, + action = ArgAction::Append, + conflicts_with = "clear_tags" + )] + tag: Option>, + #[arg(long, help = "Clear all tags on this movie", conflicts_with = "tag")] + clear_tags: bool, + }, +} + +impl From for Command { + fn from(value: RadarrEditCommand) -> Self { + Command::Radarr(RadarrCommand::Edit(value)) + } +} + +pub(super) struct RadarrEditCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: RadarrEditCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + 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(()) + } +} diff --git a/src/cli/radarr/edit_command_handler_tests.rs b/src/cli/radarr/edit_command_handler_tests.rs new file mode 100644 index 0000000..3f77649 --- /dev/null +++ b/src/cli/radarr/edit_command_handler_tests.rs @@ -0,0 +1,1445 @@ +#[cfg(test)] +mod tests { + use crate::{ + cli::{ + radarr::{edit_command_handler::RadarrEditCommand, RadarrCommand}, + Command, + }, + Cli, + }; + use clap::{error::ErrorKind, CommandFactory, Parser}; + + #[test] + fn test_radarr_edit_command_from() { + let command = RadarrEditCommand::AllIndexerSettings { + allow_hardcoded_subs: true, + disable_allow_hardcoded_subs: false, + availability_delay: None, + maximum_size: None, + minimum_age: None, + prefer_indexer_flags: true, + disable_prefer_indexer_flags: false, + retention: None, + rss_sync_interval: None, + whitelisted_subtitle_tags: None, + }; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Radarr(RadarrCommand::Edit(command))); + } + + mod cli { + use crate::models::radarr_models::MinimumAvailability; + + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[test] + fn test_edit_all_indexer_settings_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "radarr", "edit", "all-indexer-settings"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_all_indexer_settings_hardcoded_subs_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "all-indexer-settings", + "--allow-hardcoded-subs", + "--disable-allow-hardcoded-subs", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_all_indexer_settings_prefer_indexer_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "all-indexer-settings", + "--prefer-indexer-flags", + "--disable-prefer-indexer-flags", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[rstest] + fn test_edit_all_indexer_settings_assert_argument_flags_require_args( + #[values( + "--availability-delay", + "--maximum-size", + "--minimum-age", + "--retention", + "--rss-sync-interval", + "--whitelisted-subtitle-tags" + )] + flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "all-indexer-settings", + flag, + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() { + let expected_args = RadarrEditCommand::AllIndexerSettings { + allow_hardcoded_subs: false, + disable_allow_hardcoded_subs: false, + availability_delay: Some(1), + maximum_size: None, + minimum_age: None, + prefer_indexer_flags: false, + disable_prefer_indexer_flags: false, + retention: None, + rss_sync_interval: None, + whitelisted_subtitle_tags: None, + }; + let result = Cli::try_parse_from([ + "managarr", + "radarr", + "edit", + "all-indexer-settings", + "--availability-delay", + "1", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_all_indexer_settings_all_arguments_defined() { + let expected_args = 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 = Cli::try_parse_from([ + "managarr", + "radarr", + "edit", + "all-indexer-settings", + "--allow-hardcoded-subs", + "--availability-delay", + "1", + "--maximum-size", + "1", + "--minimum-age", + "1", + "--prefer-indexer-flags", + "--retention", + "1", + "--rss-sync-interval", + "1", + "--whitelisted-subtitle-tags", + "test", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_collection_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "radarr", "edit", "collection"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_collection_with_collection_id_still_requires_arguments() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "collection", + "--collection-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_collection_monitoring_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "collection", + "--collection-id", + "1", + "--enable-monitoring", + "--disable-monitoring", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_collection_search_on_add_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "collection", + "--collection-id", + "1", + "--search-on-add", + "--disable-search-on-add", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_collection_minimum_availability_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "collection", + "--collection-id", + "1", + "--minimum-availability", + "test", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[rstest] + fn test_edit_collection_assert_argument_flags_require_args( + #[values("--minimum-availability", "--quality-profile-id", "--root-folder-path")] flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "collection", + "--collection-id", + "1", + flag, + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_collection_only_requires_at_least_one_argument_plus_collection_id() { + let expected_args = RadarrEditCommand::Collection { + collection_id: 1, + enable_monitoring: false, + disable_monitoring: false, + minimum_availability: None, + quality_profile_id: None, + root_folder_path: Some("/test".to_owned()), + search_on_add: false, + disable_search_on_add: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "radarr", + "edit", + "collection", + "--collection-id", + "1", + "--root-folder-path", + "/test", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_collection_all_arguments_defined() { + let expected_args = RadarrEditCommand::Collection { + collection_id: 1, + enable_monitoring: true, + disable_monitoring: false, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/test".to_owned()), + search_on_add: true, + disable_search_on_add: false, + }; + let result = Cli::try_parse_from([ + "managarr", + "radarr", + "edit", + "collection", + "--collection-id", + "1", + "--enable-monitoring", + "--minimum-availability", + "released", + "--quality-profile-id", + "1", + "--root-folder-path", + "/test", + "--search-on-add", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_indexer_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "radarr", "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", + "radarr", + "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", + "radarr", + "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", + "radarr", + "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", + "radarr", + "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", + "radarr", + "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", + "radarr", + "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 = RadarrEditCommand::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", + "radarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--name", + "Test", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_indexer_tag_argument_is_repeatable() { + let expected_args = RadarrEditCommand::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", + "radarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--tag", + "1", + "--tag", + "2", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_indexer_all_arguments_defined() { + let expected_args = RadarrEditCommand::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", + "radarr", + "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::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_movie_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "radarr", "edit", "movie"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_movie_with_movie_id_still_requires_arguments() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "movie", + "--movie-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_movie_monitoring_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "movie", + "--movie-id", + "1", + "--enable-monitoring", + "--disable-monitoring", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_movie_tag_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "movie", + "--movie-id", + "1", + "--tag", + "1", + "--clear-tags", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[rstest] + fn test_edit_movie_assert_argument_flags_require_args( + #[values( + "--minimum-availability", + "--quality-profile-id", + "--root-folder-path", + "--tag" + )] + flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "movie", + "--movie-id", + "1", + flag, + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_movie_minimum_availability_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "edit", + "movie", + "--movie-id", + "1", + "--minimum-availability", + "test", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_movie_only_requires_at_least_one_argument_plus_movie_id() { + let expected_args = RadarrEditCommand::Movie { + movie_id: 1, + enable_monitoring: false, + disable_monitoring: false, + minimum_availability: None, + quality_profile_id: None, + root_folder_path: Some("/nfs/test".to_owned()), + tag: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "radarr", + "edit", + "movie", + "--movie-id", + "1", + "--root-folder-path", + "/nfs/test", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_movie_tag_argument_is_repeatable() { + let expected_args = RadarrEditCommand::Movie { + movie_id: 1, + enable_monitoring: false, + disable_monitoring: false, + minimum_availability: None, + quality_profile_id: None, + root_folder_path: None, + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "radarr", + "edit", + "movie", + "--movie-id", + "1", + "--tag", + "1", + "--tag", + "2", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_movie_all_arguments_defined() { + let expected_args = RadarrEditCommand::Movie { + movie_id: 1, + enable_monitoring: true, + disable_monitoring: false, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "radarr", + "edit", + "movie", + "--movie-id", + "1", + "--enable-monitoring", + "--minimum-availability", + "released", + "--quality-profile-id", + "1", + "--root-folder-path", + "/nfs/test", + "--tag", + "1", + "--tag", + "2", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_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::edit_command_handler::{RadarrEditCommand, RadarrEditCommandHandler}, + CliCommandHandler, + }, + models::{ + radarr_models::{ + EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings, + MinimumAvailability, RadarrSerdeable, + }, + Serdeable, + }, + network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent}, + }; + + #[tokio::test] + async fn test_handle_edit_all_indexer_settings_command() { + 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::( + 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::( + 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 = 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 = RadarrEditCommandHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_all_indexer_settings_command_disable_flags_function_properly() { + let expected_edit_all_indexer_settings = IndexerSettings { + allow_hardcoded_subs: false, + availability_delay: 1, + id: 1, + maximum_size: 1, + minimum_age: 1, + prefer_indexer_flags: false, + 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::( + RadarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings( + IndexerSettings { + allow_hardcoded_subs: true, + availability_delay: 2, + id: 1, + maximum_size: 2, + minimum_age: 2, + prefer_indexer_flags: true, + retention: 2, + rss_sync_interval: 2, + whitelisted_hardcoded_subs: "testing".into(), + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::( + 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 = RadarrEditCommand::AllIndexerSettings { + allow_hardcoded_subs: false, + disable_allow_hardcoded_subs: true, + availability_delay: Some(1), + maximum_size: Some(1), + minimum_age: Some(1), + prefer_indexer_flags: false, + disable_prefer_indexer_flags: true, + retention: Some(1), + rss_sync_interval: Some(1), + whitelisted_subtitle_tags: Some("test".to_owned()), + }; + + let result = RadarrEditCommandHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_all_indexer_settings_command_unprovided_values_default_to_previous_values( + ) { + let expected_edit_all_indexer_settings = IndexerSettings { + allow_hardcoded_subs: true, + availability_delay: 2, + id: 1, + maximum_size: 2, + minimum_age: 2, + prefer_indexer_flags: true, + retention: 2, + rss_sync_interval: 2, + whitelisted_hardcoded_subs: "testing".into(), + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings( + IndexerSettings { + allow_hardcoded_subs: true, + availability_delay: 2, + id: 1, + maximum_size: 2, + minimum_age: 2, + prefer_indexer_flags: true, + retention: 2, + rss_sync_interval: 2, + whitelisted_hardcoded_subs: "testing".into(), + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::( + 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 = RadarrEditCommand::AllIndexerSettings { + allow_hardcoded_subs: false, + disable_allow_hardcoded_subs: false, + availability_delay: None, + maximum_size: None, + minimum_age: None, + prefer_indexer_flags: false, + disable_prefer_indexer_flags: false, + retention: None, + rss_sync_interval: None, + whitelisted_subtitle_tags: None, + }; + + let result = RadarrEditCommandHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_collection_command() { + let expected_edit_collection_params = EditCollectionParams { + collection_id: 1, + monitored: Some(true), + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + search_on_add: Some(true), + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_collection_command = RadarrEditCommand::Collection { + collection_id: 1, + enable_monitoring: true, + disable_monitoring: false, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + search_on_add: true, + disable_search_on_add: false, + }; + + let result = + RadarrEditCommandHandler::with(&app_arc, edit_collection_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_collection_command_handles_disable_flags_properly() { + let expected_edit_collection_params = EditCollectionParams { + collection_id: 1, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + search_on_add: Some(false), + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_collection_command = RadarrEditCommand::Collection { + collection_id: 1, + enable_monitoring: false, + disable_monitoring: true, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + search_on_add: false, + disable_search_on_add: true, + }; + + let result = + RadarrEditCommandHandler::with(&app_arc, edit_collection_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_collection_command_no_boolean_flags_returns_none_value() { + let expected_edit_collection_params = EditCollectionParams { + collection_id: 1, + monitored: None, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + search_on_add: None, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_collection_command = RadarrEditCommand::Collection { + collection_id: 1, + enable_monitoring: false, + disable_monitoring: false, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + search_on_add: false, + disable_search_on_add: false, + }; + + let result = + RadarrEditCommandHandler::with(&app_arc, edit_collection_command, &mut mock_network) + .handle() + .await; + + 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::( + RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_indexer_command = RadarrEditCommand::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 = + RadarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_indexer_command_handles_disable_flags_properly() { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + 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::( + RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_indexer_command = RadarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: false, + disable_rss: true, + enable_automatic_search: false, + disable_automatic_search: true, + enable_interactive_search: false, + disable_interactive_search: true, + 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 = + RadarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_indexer_command_no_boolean_flags_returns_none_value() { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: None, + enable_automatic_search: None, + enable_interactive_search: None, + 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::( + RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_indexer_command = RadarrEditCommand::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: 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 = + RadarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_movie_command() { + let expected_edit_movie_params = EditMovieParams { + movie_id: 1, + monitored: Some(true), + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_movie_command = RadarrEditCommand::Movie { + movie_id: 1, + enable_monitoring: true, + disable_monitoring: false, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = RadarrEditCommandHandler::with(&app_arc, edit_movie_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_movie_command_handles_disable_monitoring_flag_properly() { + let expected_edit_movie_params = EditMovieParams { + movie_id: 1, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_movie_command = RadarrEditCommand::Movie { + movie_id: 1, + enable_monitoring: false, + disable_monitoring: true, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = RadarrEditCommandHandler::with(&app_arc, edit_movie_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_movie_command_no_monitoring_boolean_flags_returns_none_value() { + let expected_edit_movie_params = EditMovieParams { + movie_id: 1, + monitored: None, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_movie_command = RadarrEditCommand::Movie { + movie_id: 1, + enable_monitoring: false, + disable_monitoring: false, + minimum_availability: Some(MinimumAvailability::Released), + quality_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = RadarrEditCommandHandler::with(&app_arc, edit_movie_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + } +} diff --git a/src/cli/radarr/get_command_handler.rs b/src/cli/radarr/get_command_handler.rs new file mode 100644 index 0000000..35004e4 --- /dev/null +++ b/src/cli/radarr/get_command_handler.rs @@ -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 for Command { + fn from(value: RadarrGetCommand) -> Self { + Command::Radarr(RadarrCommand::Get(value)) + } +} + +pub(super) struct RadarrGetCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: RadarrGetCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + 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(()) + } +} diff --git a/src/cli/radarr/get_command_handler_tests.rs b/src/cli/radarr/get_command_handler_tests.rs new file mode 100644 index 0000000..eb9af0f --- /dev/null +++ b/src/cli/radarr/get_command_handler_tests.rs @@ -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::( + 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::( + 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::( + 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::(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()); + } + } +} diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs new file mode 100644 index 0000000..e536d7f --- /dev/null +++ b/src/cli/radarr/list_command_handler.rs @@ -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 for Command { + fn from(value: RadarrListCommand) -> Self { + Command::Radarr(RadarrCommand::List(value)) + } +} + +pub(super) struct RadarrListCommandHandler<'a, 'b> { + app: &'a Arc>>, + command: RadarrListCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandHandler<'a, 'b> { + fn with( + app: &'a Arc>>, + 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(()) + } +} diff --git a/src/cli/radarr/list_command_handler_tests.rs b/src/cli/radarr/list_command_handler_tests.rs new file mode 100644 index 0000000..4c7446a --- /dev/null +++ b/src/cli/radarr/list_command_handler_tests.rs @@ -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::(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::( + 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::( + 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()); + } + } +} diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs new file mode 100644 index 0000000..9bc654c --- /dev/null +++ b/src/cli/radarr/mod.rs @@ -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 for Command { + fn from(radarr_command: RadarrCommand) -> Command { + Command::Radarr(radarr_command) + } +} + +pub(super) struct RadarrCliHandler<'a, 'b> { + app: &'a Arc>>, + command: RadarrCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, 'b> { + fn with( + app: &'a Arc>>, + 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(()) + } +} diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs new file mode 100644 index 0000000..88bf862 --- /dev/null +++ b/src/cli/radarr/radarr_command_tests.rs @@ -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::(RadarrEvent::GetBlocklist.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse( + BlocklistResponse { + records: vec![BlocklistItem::default()], + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::(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::( + 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::( + 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::( + 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::( + 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::( + 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::(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::( + 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::( + 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::( + 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::( + 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::( + 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::( + 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::( + 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::( + 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()); + } + } +} diff --git a/src/cli/radarr/refresh_command_handler.rs b/src/cli/radarr/refresh_command_handler.rs new file mode 100644 index 0000000..5bb0e73 --- /dev/null +++ b/src/cli/radarr/refresh_command_handler.rs @@ -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 for Command { + fn from(value: RadarrRefreshCommand) -> Self { + Command::Radarr(RadarrCommand::Refresh(value)) + } +} + +pub(super) struct RadarrRefreshCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: RadarrRefreshCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand> + for RadarrRefreshCommandHandler<'a, 'b> +{ + fn with( + _app: &'a Arc>>, + 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(()) + } +} diff --git a/src/cli/radarr/refresh_command_handler_tests.rs b/src/cli/radarr/refresh_command_handler_tests.rs new file mode 100644 index 0000000..2f8352a --- /dev/null +++ b/src/cli/radarr/refresh_command_handler_tests.rs @@ -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::(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::( + 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()); + } + } +} diff --git a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs index c982452..5dae8fc 100644 --- a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs @@ -410,7 +410,7 @@ mod tests { #[case( ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::DeleteBlocklistItemPrompt, - RadarrEvent::DeleteBlocklistItem + RadarrEvent::DeleteBlocklistItem(None) )] #[case( ActiveRadarrBlock::Blocklist, diff --git a/src/handlers/radarr_handlers/blocklist/mod.rs b/src/handlers/radarr_handlers/blocklist/mod.rs index fd6e4dc..f67d3b4 100644 --- a/src/handlers/radarr_handlers/blocklist/mod.rs +++ b/src/handlers/radarr_handlers/blocklist/mod.rs @@ -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(); diff --git a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs index 61c2b6f..2134064 100644 --- a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs @@ -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 ); } diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs index 2cfd8ea..d7bb213 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs @@ -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; } diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs index 4790569..4aaed4e 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs @@ -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); } diff --git a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs index 15a349e..0edcc0a 100644 --- a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs +++ b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs @@ -247,7 +247,7 @@ mod tests { #[case( ActiveRadarrBlock::Downloads, ActiveRadarrBlock::DeleteDownloadPrompt, - RadarrEvent::DeleteDownload + RadarrEvent::DeleteDownload(None) )] #[case( ActiveRadarrBlock::Downloads, diff --git a/src/handlers/radarr_handlers/downloads/mod.rs b/src/handlers/radarr_handlers/downloads/mod.rs index d23184a..b6d4caa 100644 --- a/src/handlers/radarr_handlers/downloads/mod.rs +++ b/src/handlers/radarr_handlers/downloads/mod.rs @@ -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(); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs index 465a639..44ff33b 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs @@ -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; diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs index a76156f..c53b708 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -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)) ); } diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs index 2869309..8bb7c91 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -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; diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs index 40fd94f..23c723b 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -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, ); diff --git a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs index d8e3696..c1fda63 100644 --- a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs @@ -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, diff --git a/src/handlers/radarr_handlers/indexers/mod.rs b/src/handlers/radarr_handlers/indexers/mod.rs index 99cccdd..c5757de 100644 --- a/src/handlers/radarr_handlers/indexers/mod.rs +++ b/src/handlers/radarr_handlers/indexers/mod.rs @@ -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); } diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index e4d24d0..93b524a 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -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(); diff --git a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs index 7718462..875a7f4 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs @@ -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()); } diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler.rs b/src/handlers/radarr_handlers/library/delete_movie_handler.rs index 036763c..fa8a956 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler.rs @@ -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(); diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs index 5eeafb8..9eaa7f1 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs @@ -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); diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler.rs b/src/handlers/radarr_handlers/library/edit_movie_handler.rs index d583a71..3419095 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler.rs @@ -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; } diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs index 030f531..5386a5d 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs @@ -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); diff --git a/src/handlers/radarr_handlers/library/library_handler_tests.rs b/src/handlers/radarr_handlers/library/library_handler_tests.rs index 32aee4f..0142691 100644 --- a/src/handlers/radarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/library_handler_tests.rs @@ -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 ); } diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index fc6e798..26bbcc9 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -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(); diff --git a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs index e0013b6..326291a 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs @@ -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, diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index 375b7ab..5641d83 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -140,7 +140,7 @@ mod tests { #[values( ActiveRadarrBlock::DeleteIndexerPrompt, ActiveRadarrBlock::Indexers, - ActiveRadarrBlock::IndexerSettingsPrompt, + ActiveRadarrBlock::AllIndexerSettingsPrompt, ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, ActiveRadarrBlock::IndexerSettingsConfirmPrompt, ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, diff --git a/src/handlers/radarr_handlers/root_folders/mod.rs b/src/handlers/radarr_handlers/root_folders/mod.rs index 0b34f79..4422df0 100644 --- a/src/handlers/radarr_handlers/root_folders/mod.rs +++ b/src/handlers/radarr_handlers/root_folders/mod.rs @@ -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(); diff --git a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs index 9cce9ff..e9840db 100644 --- a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs @@ -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(), diff --git a/src/handlers/radarr_handlers/system/system_details_handler.rs b/src/handlers/radarr_handlers/system/system_details_handler.rs index 6420792..00f55c9 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler.rs @@ -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(); diff --git a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs index 87d3eca..5d22296 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs @@ -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(), diff --git a/src/main.rs b/src/main.rs index 13371dd..504a4f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, +} + #[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>>) -> 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>>) -> 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 { diff --git a/src/models/mod.rs b/src/models/mod.rs index d6e2c1e..7193066 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -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, + 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 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::>(); write!( f, "{}", - text_vec[*self.offset.borrow()..] + text_vec[self.offset.load(Ordering::SeqCst)..] .iter() .cloned() .collect::() @@ -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) + } + } + )* + } +} diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index 4b5f8a3..7cd2696 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -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] diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 922d089..0f08ff8 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -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, } -#[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>, } -#[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, } +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct EditCollectionParams { + pub collection_id: i64, + pub monitored: Option, + pub minimum_availability: Option, + pub quality_profile_id: Option, + pub root_folder_path: Option, + pub search_on_add: Option, +} + +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct EditIndexerParams { + pub indexer_id: i64, + pub name: Option, + pub enable_rss: Option, + pub enable_automatic_search: Option, + pub enable_interactive_search: Option, + pub url: Option, + pub api_key: Option, + pub seed_ratio: Option, + pub tags: Option>, + pub priority: Option, + 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, + pub minimum_availability: Option, + pub quality_profile_id: Option, + pub root_folder_path: Option, + pub tags: Option>, + 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, } -#[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, @@ -257,12 +307,12 @@ pub struct Log { pub method: Option, } -#[derive(Default, Deserialize, Debug, Eq, PartialEq)] +#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct LogResponse { pub records: Vec, } -#[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, } -#[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, @@ -393,7 +445,7 @@ pub struct MovieCommandBody { pub movie_ids: Vec, } -#[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, } -#[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, } -#[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, @@ -457,7 +518,7 @@ pub struct RatingsList { pub rotten_tomatoes: Option, } -#[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>, } -#[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, } -#[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, @@ -525,13 +586,39 @@ pub struct Task { pub next_execution: DateTime, } -#[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>, pub fixed: Option>, } + +#[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), + Credits(Vec), + DiskSpaces(Vec), + DownloadsResponse(DownloadsResponse), + Indexers(Vec), + IndexerSettings(IndexerSettings), + LogResponse(LogResponse), + Movie(Movie), + MovieHistoryItems(Vec), + Movies(Vec), + QualityProfiles(Vec), + QueueEvents(Vec), + Releases(Vec), + RootFolders(Vec), + SystemStatus(SystemStatus), + Tags(Vec), + Tasks(Vec), + Updates(Vec), + AddMovieSearchResults(Vec), + IndexerTestResults(Vec), +} + +impl From 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), + Credits(Vec), + DiskSpaces(Vec), + DownloadsResponse(DownloadsResponse), + Indexers(Vec), + IndexerSettings(IndexerSettings), + LogResponse(LogResponse), + Movie(Movie), + MovieHistoryItems(Vec), + Movies(Vec), + QualityProfiles(Vec), + QueueEvents(Vec), + Releases(Vec), + RootFolders(Vec), + SystemStatus(SystemStatus), + Tags(Vec), + Tasks(Vec), + Updates(Vec), + AddMovieSearchResults(Vec), + IndexerTestResults(Vec), + } +); diff --git a/src/models/radarr_models_tests.rs b/src/models/radarr_models_tests.rs index 3c8ba8c..e562eb1 100644 --- a/src/models/radarr_models_tests.rs +++ b/src/models/radarr_models_tests.rs @@ -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) + ); + } } diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index 83a2aa4..e422670 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -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, diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index e983dcb..a487156 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -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) ); diff --git a/src/network/mod.rs b/src/network/mod.rs index 216f793..df254ae 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -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; +} + +#[derive(Clone)] pub struct Network<'a, 'b> { client: Client, cancellation_token: CancellationToken, pub app: &'a Arc>>, } +#[async_trait] +impl<'a, 'b> NetworkTrait for Network<'a, 'b> { + async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result { + 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>>, 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( + async fn handle_request( &mut self, request_props: RequestProps, mut app_update_fn: impl FnMut(R, MutexGuard<'_, App<'_>>), - ) where + ) -> Result + 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::(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} ")) } } } diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index f3b3e18..3296abf 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -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::>()[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::( 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, } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index cc76d70..648660a 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -1,4 +1,4 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Result}; use std::fmt::Debug; use indoc::formatdoc; @@ -10,10 +10,11 @@ use urlencoding::encode; use crate::app::RadarrConfig; use crate::models::radarr_models::{ AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, BlocklistResponse, Collection, - CollectionMovie, CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, - Indexer, IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody, - MovieHistoryItem, QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder, - SystemStatus, Tag, Task, Update, + CollectionMovie, CommandBody, Credit, CreditType, DeleteMovieParams, DiskSpace, DownloadRecord, + DownloadsResponse, EditCollectionParams, EditIndexerParams, EditMovieParams, Indexer, + IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody, MovieHistoryItem, + QualityProfile, QueueEvent, RadarrSerdeable, Release, ReleaseDownloadBody, RootFolder, + SystemStatus, Tag, Task, TaskName, Update, }; use crate::models::servarr_data::radarr::modals::{ AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem, @@ -29,89 +30,93 @@ use crate::utils::{convert_runtime, convert_to_gb}; #[path = "radarr_network_tests.rs"] mod radarr_network_tests; -#[derive(Debug, Eq, PartialEq, Clone, Copy)] +#[derive(Debug, Eq, PartialEq, Clone)] pub enum RadarrEvent { - AddMovie, - AddRootFolder, + AddMovie(Option), + AddRootFolder(Option), + AddTag(String), ClearBlocklist, - DeleteBlocklistItem, - DeleteDownload, - DeleteIndexer, - DeleteMovie, - DeleteRootFolder, - DownloadRelease, - EditAllIndexerSettings, - EditCollection, - EditIndexer, - EditMovie, + DeleteBlocklistItem(Option), + DeleteDownload(Option), + DeleteIndexer(Option), + DeleteMovie(Option), + DeleteRootFolder(Option), + DeleteTag(i64), + DownloadRelease(Option), + EditAllIndexerSettings(Option), + EditCollection(Option), + EditIndexer(Option), + EditMovie(Option), GetBlocklist, GetCollections, GetDownloads, GetIndexers, - GetIndexerSettings, - GetLogs, - GetMovieCredits, - GetMovieDetails, - GetMovieHistory, + GetAllIndexerSettings, + GetLogs(Option), + GetMovieCredits(Option), + GetMovieDetails(Option), + GetMovieHistory(Option), GetMovies, GetOverview, GetQualityProfiles, GetQueuedEvents, - GetReleases, + GetReleases(Option), GetRootFolders, GetStatus, GetTags, GetTasks, GetUpdates, HealthCheck, - SearchNewMovie, - StartTask, - TestIndexer, + SearchNewMovie(Option), + StartTask(Option), + TestIndexer(Option), TestAllIndexers, - TriggerAutomaticSearch, + TriggerAutomaticSearch(Option), UpdateAllMovies, - UpdateAndScan, + UpdateAndScan(Option), UpdateCollections, UpdateDownloads, } impl RadarrEvent { - const fn resource(self) -> &'static str { - match self { + const fn resource(&self) -> &'static str { + match &self { RadarrEvent::ClearBlocklist => "/blocklist/bulk", - RadarrEvent::DeleteBlocklistItem => "/blocklist", + RadarrEvent::DeleteBlocklistItem(_) => "/blocklist", RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", - RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", - RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", - RadarrEvent::GetIndexers | RadarrEvent::EditIndexer | RadarrEvent::DeleteIndexer => { + RadarrEvent::GetCollections | RadarrEvent::EditCollection(_) => "/collection", + RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload(_) => "/queue", + RadarrEvent::GetIndexers | RadarrEvent::EditIndexer(_) | RadarrEvent::DeleteIndexer(_) => { "/indexer" } - RadarrEvent::GetIndexerSettings | RadarrEvent::EditAllIndexerSettings => "/config/indexer", - RadarrEvent::GetLogs => "/log", - RadarrEvent::AddMovie - | RadarrEvent::EditMovie + RadarrEvent::GetAllIndexerSettings | RadarrEvent::EditAllIndexerSettings(_) => { + "/config/indexer" + } + RadarrEvent::GetLogs(_) => "/log", + RadarrEvent::AddMovie(_) + | RadarrEvent::EditMovie(_) | RadarrEvent::GetMovies - | RadarrEvent::GetMovieDetails - | RadarrEvent::DeleteMovie => "/movie", - RadarrEvent::SearchNewMovie => "/movie/lookup", - RadarrEvent::GetMovieCredits => "/credit", - RadarrEvent::GetMovieHistory => "/history/movie", + | RadarrEvent::GetMovieDetails(_) + | RadarrEvent::DeleteMovie(_) => "/movie", + RadarrEvent::SearchNewMovie(_) => "/movie/lookup", + RadarrEvent::GetMovieCredits(_) => "/credit", + RadarrEvent::GetMovieHistory(_) => "/history/movie", RadarrEvent::GetOverview => "/diskspace", RadarrEvent::GetQualityProfiles => "/qualityprofile", - RadarrEvent::GetReleases | RadarrEvent::DownloadRelease => "/release", - RadarrEvent::AddRootFolder | RadarrEvent::GetRootFolders | RadarrEvent::DeleteRootFolder => { - "/rootfolder" - } + RadarrEvent::GetReleases(_) | RadarrEvent::DownloadRelease(_) => "/release", + RadarrEvent::AddRootFolder(_) + | RadarrEvent::GetRootFolders + | RadarrEvent::DeleteRootFolder(_) => "/rootfolder", RadarrEvent::GetStatus => "/system/status", - RadarrEvent::GetTags => "/tag", + RadarrEvent::GetTags | RadarrEvent::AddTag(_) | RadarrEvent::DeleteTag(_) => "/tag", RadarrEvent::GetTasks => "/system/task", RadarrEvent::GetUpdates => "/update", - RadarrEvent::TestIndexer => "/indexer/test", + RadarrEvent::TestIndexer(_) => "/indexer/test", RadarrEvent::TestAllIndexers => "/indexer/testall", - RadarrEvent::StartTask + RadarrEvent::StartTask(_) | RadarrEvent::GetQueuedEvents - | RadarrEvent::TriggerAutomaticSearch - | RadarrEvent::UpdateAndScan + | RadarrEvent::TriggerAutomaticSearch(_) + | RadarrEvent::UpdateAndScan(_) | RadarrEvent::UpdateAllMovies | RadarrEvent::UpdateDownloads | RadarrEvent::UpdateCollections => "/command", @@ -127,56 +132,118 @@ impl From for NetworkEvent { } impl<'a, 'b> Network<'a, 'b> { - pub async fn handle_radarr_event(&mut self, radarr_event: RadarrEvent) { + pub async fn handle_radarr_event( + &mut self, + radarr_event: RadarrEvent, + ) -> Result { match radarr_event { - RadarrEvent::AddMovie => self.add_movie().await, - RadarrEvent::AddRootFolder => self.add_root_folder().await, - RadarrEvent::ClearBlocklist => self.clear_blocklist().await, - RadarrEvent::DeleteBlocklistItem => self.delete_blocklist_item().await, - RadarrEvent::DeleteDownload => self.delete_download().await, - RadarrEvent::DeleteIndexer => self.delete_indexer().await, - RadarrEvent::DeleteMovie => self.delete_movie().await, - RadarrEvent::DeleteRootFolder => self.delete_root_folder().await, - RadarrEvent::DownloadRelease => self.download_release().await, - RadarrEvent::EditAllIndexerSettings => self.edit_all_indexer_settings().await, - RadarrEvent::EditCollection => self.edit_collection().await, - RadarrEvent::EditIndexer => self.edit_indexer().await, - RadarrEvent::EditMovie => self.edit_movie().await, - RadarrEvent::GetBlocklist => self.get_blocklist().await, - RadarrEvent::GetCollections => self.get_collections().await, - RadarrEvent::GetDownloads => self.get_downloads().await, - RadarrEvent::GetIndexers => self.get_indexers().await, - RadarrEvent::GetIndexerSettings => self.get_indexer_settings().await, - RadarrEvent::GetLogs => self.get_logs().await, - RadarrEvent::GetMovieCredits => self.get_credits().await, - RadarrEvent::GetMovieDetails => self.get_movie_details().await, - RadarrEvent::GetMovieHistory => self.get_movie_history().await, - RadarrEvent::GetMovies => self.get_movies().await, - RadarrEvent::GetOverview => self.get_diskspace().await, - RadarrEvent::GetQualityProfiles => self.get_quality_profiles().await, - RadarrEvent::GetQueuedEvents => self.get_queued_events().await, - RadarrEvent::GetReleases => self.get_releases().await, - RadarrEvent::GetRootFolders => self.get_root_folders().await, - RadarrEvent::GetStatus => self.get_status().await, - RadarrEvent::GetTags => self.get_tags().await, - RadarrEvent::GetTasks => self.get_tasks().await, - RadarrEvent::GetUpdates => self.get_updates().await, - RadarrEvent::HealthCheck => self.get_healthcheck().await, - RadarrEvent::SearchNewMovie => self.search_movie().await, - RadarrEvent::StartTask => self.start_task().await, - RadarrEvent::TestIndexer => self.test_indexer().await, - RadarrEvent::TestAllIndexers => self.test_all_indexers().await, - RadarrEvent::TriggerAutomaticSearch => self.trigger_automatic_search().await, - RadarrEvent::UpdateAllMovies => self.update_all_movies().await, - RadarrEvent::UpdateAndScan => self.update_and_scan().await, - RadarrEvent::UpdateCollections => self.update_collections().await, - RadarrEvent::UpdateDownloads => self.update_downloads().await, + RadarrEvent::AddMovie(body) => self.add_movie(body).await.map(RadarrSerdeable::from), + RadarrEvent::AddRootFolder(path) => { + self.add_root_folder(path).await.map(RadarrSerdeable::from) + } + RadarrEvent::AddTag(tag) => self.add_tag(tag).await.map(RadarrSerdeable::from), + RadarrEvent::ClearBlocklist => self.clear_blocklist().await.map(RadarrSerdeable::from), + RadarrEvent::DeleteBlocklistItem(blocklist_item_id) => self + .delete_blocklist_item(blocklist_item_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::DeleteDownload(download_id) => self + .delete_download(download_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::DeleteIndexer(indexer_id) => self + .delete_indexer(indexer_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::DeleteMovie(params) => { + self.delete_movie(params).await.map(RadarrSerdeable::from) + } + RadarrEvent::DeleteRootFolder(root_folder_id) => self + .delete_root_folder(root_folder_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::DeleteTag(tag_id) => self.delete_tag(tag_id).await.map(RadarrSerdeable::from), + RadarrEvent::DownloadRelease(params) => self + .download_release(params) + .await + .map(RadarrSerdeable::from), + RadarrEvent::EditAllIndexerSettings(params) => self + .edit_all_indexer_settings(params) + .await + .map(RadarrSerdeable::from), + RadarrEvent::EditCollection(params) => self + .edit_collection(params) + .await + .map(RadarrSerdeable::from), + RadarrEvent::EditIndexer(params) => { + self.edit_indexer(params).await.map(RadarrSerdeable::from) + } + RadarrEvent::EditMovie(params) => self.edit_movie(params).await.map(RadarrSerdeable::from), + RadarrEvent::GetBlocklist => self.get_blocklist().await.map(RadarrSerdeable::from), + RadarrEvent::GetCollections => self.get_collections().await.map(RadarrSerdeable::from), + RadarrEvent::GetDownloads => self.get_downloads().await.map(RadarrSerdeable::from), + RadarrEvent::GetIndexers => self.get_indexers().await.map(RadarrSerdeable::from), + RadarrEvent::GetAllIndexerSettings => self + .get_all_indexer_settings() + .await + .map(RadarrSerdeable::from), + RadarrEvent::GetLogs(events) => self.get_logs(events).await.map(RadarrSerdeable::from), + RadarrEvent::GetMovieCredits(movie_id) => { + self.get_credits(movie_id).await.map(RadarrSerdeable::from) + } + RadarrEvent::GetMovieDetails(movie_id) => self + .get_movie_details(movie_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::GetMovieHistory(movie_id) => self + .get_movie_history(movie_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::GetMovies => self.get_movies().await.map(RadarrSerdeable::from), + RadarrEvent::GetOverview => self.get_diskspace().await.map(RadarrSerdeable::from), + RadarrEvent::GetQualityProfiles => { + self.get_quality_profiles().await.map(RadarrSerdeable::from) + } + RadarrEvent::GetQueuedEvents => self.get_queued_events().await.map(RadarrSerdeable::from), + RadarrEvent::GetReleases(movie_id) => { + self.get_releases(movie_id).await.map(RadarrSerdeable::from) + } + RadarrEvent::GetRootFolders => self.get_root_folders().await.map(RadarrSerdeable::from), + RadarrEvent::GetStatus => self.get_status().await.map(RadarrSerdeable::from), + RadarrEvent::GetTags => self.get_tags().await.map(RadarrSerdeable::from), + RadarrEvent::GetTasks => self.get_tasks().await.map(RadarrSerdeable::from), + RadarrEvent::GetUpdates => self.get_updates().await.map(RadarrSerdeable::from), + RadarrEvent::HealthCheck => self.get_healthcheck().await.map(RadarrSerdeable::from), + RadarrEvent::SearchNewMovie(query) => { + self.search_movie(query).await.map(RadarrSerdeable::from) + } + RadarrEvent::StartTask(task_name) => { + self.start_task(task_name).await.map(RadarrSerdeable::from) + } + RadarrEvent::TestIndexer(indexer_id) => self + .test_indexer(indexer_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::TestAllIndexers => self.test_all_indexers().await.map(RadarrSerdeable::from), + RadarrEvent::TriggerAutomaticSearch(movie_id) => self + .trigger_automatic_search(movie_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::UpdateAllMovies => self.update_all_movies().await.map(RadarrSerdeable::from), + RadarrEvent::UpdateAndScan(movie_id) => self + .update_and_scan(movie_id) + .await + .map(RadarrSerdeable::from), + RadarrEvent::UpdateCollections => self.update_collections().await.map(RadarrSerdeable::from), + RadarrEvent::UpdateDownloads => self.update_downloads().await.map(RadarrSerdeable::from), } } - async fn add_movie(&mut self) { + async fn add_movie(&mut self, add_movie_body_option: Option) -> Result { info!("Adding new movie to Radarr"); - let body = { + let body = if let Some(add_movie_body) = add_movie_body_option { + add_movie_body + } else { let tags = self .app .lock() @@ -266,7 +333,7 @@ impl<'a, 'b> Network<'a, 'b> { let request_props = self .radarr_request_props_from( - RadarrEvent::AddMovie.resource(), + RadarrEvent::AddMovie(None).resource(), RequestMethod::Post, Some(body), ) @@ -274,12 +341,14 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn add_root_folder(&mut self) { + async fn add_root_folder(&mut self, root_folder: Option) -> Result { info!("Adding new root folder to Radarr"); - let body = { + let body = if let Some(path) = root_folder { + AddRootFolderBody { path } + } else { let mut app = self.app.lock().await; let path = app .data @@ -299,7 +368,7 @@ impl<'a, 'b> Network<'a, 'b> { let request_props = self .radarr_request_props_from( - RadarrEvent::AddRootFolder.resource(), + RadarrEvent::AddRootFolder(None).resource(), RequestMethod::Post, Some(body), ) @@ -307,15 +376,15 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn add_tag(&mut self, tag: String) { + async fn add_tag(&mut self, tag: String) -> Result { info!("Adding a new Radarr tag"); let request_props = self .radarr_request_props_from( - RadarrEvent::GetTags.resource(), + RadarrEvent::AddTag(String::new()).resource(), RequestMethod::Post, Some(json!({ "label": tag })), ) @@ -325,10 +394,26 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::(request_props, |tag, mut app| { app.data.radarr_data.tags_map.insert(tag.id, tag.label); }) - .await; + .await } - async fn clear_blocklist(&mut self) { + async fn delete_tag(&mut self, id: i64) -> Result<()> { + info!("Deleting Radarr tag with id: {id}"); + + let request_props = self + .radarr_request_props_from( + format!("{}/{id}", RadarrEvent::DeleteTag(id).resource()).as_str(), + RequestMethod::Delete, + None::<()>, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + async fn clear_blocklist(&mut self) -> Result<()> { info!("Clearing Radarr blocklist"); let ids = self @@ -353,29 +438,29 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn delete_blocklist_item(&mut self) { - let blocklist_item_id = self - .app - .lock() - .await - .data - .radarr_data - .blocklist - .current_selection() - .id; + async fn delete_blocklist_item(&mut self, blocklist_item_id: Option) -> Result<()> { + let id = if let Some(b_id) = blocklist_item_id { + b_id + } else { + self + .app + .lock() + .await + .data + .radarr_data + .blocklist + .current_selection() + .id + }; - info!("Deleting Radarr blocklist item for item with id: {blocklist_item_id}"); + info!("Deleting Radarr blocklist item for item with id: {id}"); let request_props = self .radarr_request_props_from( - format!( - "{}/{blocklist_item_id}", - RadarrEvent::DeleteBlocklistItem.resource() - ) - .as_str(), + format!("{}/{id}", RadarrEvent::DeleteBlocklistItem(None).resource()).as_str(), RequestMethod::Delete, None::<()>, ) @@ -383,25 +468,29 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::<(), ()>(request_props, |_, _| ()) - .await; + .await } - async fn delete_download(&mut self) { - let download_id = self - .app - .lock() - .await - .data - .radarr_data - .downloads - .current_selection() - .id; + async fn delete_download(&mut self, download_id: Option) -> Result<()> { + let id = if let Some(dl_id) = download_id { + dl_id + } else { + self + .app + .lock() + .await + .data + .radarr_data + .downloads + .current_selection() + .id + }; - info!("Deleting Radarr download for download with id: {download_id}"); + info!("Deleting Radarr download for download with id: {id}"); let request_props = self .radarr_request_props_from( - format!("{}/{download_id}", RadarrEvent::DeleteDownload.resource()).as_str(), + format!("{}/{id}", RadarrEvent::DeleteDownload(None).resource()).as_str(), RequestMethod::Delete, None::<()>, ) @@ -409,25 +498,29 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::<(), ()>(request_props, |_, _| ()) - .await; + .await } - async fn delete_indexer(&mut self) { - let indexer_id = self - .app - .lock() - .await - .data - .radarr_data - .indexers - .current_selection() - .id; + async fn delete_indexer(&mut self, indexer_id: Option) -> Result<()> { + let id = if let Some(i_id) = indexer_id { + i_id + } else { + self + .app + .lock() + .await + .data + .radarr_data + .indexers + .current_selection() + .id + }; - info!("Deleting Radarr indexer for indexer with id: {indexer_id}"); + info!("Deleting Radarr indexer for indexer with id: {id}"); let request_props = self .radarr_request_props_from( - format!("{}/{indexer_id}", RadarrEvent::DeleteIndexer.resource()).as_str(), + format!("{}/{id}", RadarrEvent::DeleteIndexer(None).resource()).as_str(), RequestMethod::Delete, None::<()>, ) @@ -435,21 +528,31 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::<(), ()>(request_props, |_, _| ()) - .await; + .await } - async fn delete_movie(&mut self) { - let (movie_id, tmdb_id) = self.extract_movie_id().await; - let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; - let add_import_exclusion = self.app.lock().await.data.radarr_data.add_list_exclusion; + async fn delete_movie(&mut self, delete_movie_params: Option) -> Result<()> { + let (movie_id, delete_files, add_import_exclusion) = if let Some(params) = delete_movie_params { + ( + params.id, + params.delete_movie_files, + params.add_list_exclusion, + ) + } else { + let movie_id = self.extract_movie_id().await; + let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; + let add_import_exclusion = self.app.lock().await.data.radarr_data.add_list_exclusion; - info!("Deleting Radarr movie with tmdb_id {tmdb_id} and Radarr id: {movie_id} with deleteFiles={delete_files} and addImportExclusion={add_import_exclusion}"); + (movie_id, delete_files, add_import_exclusion) + }; + + info!("Deleting Radarr movie with ID: {movie_id} with deleteFiles={delete_files} and addImportExclusion={add_import_exclusion}"); let request_props = self .radarr_request_props_from( format!( "{}/{movie_id}?deleteFiles={delete_files}&addImportExclusion={add_import_exclusion}", - RadarrEvent::DeleteMovie.resource() + RadarrEvent::DeleteMovie(None).resource() ) .as_str(), RequestMethod::Delete, @@ -457,7 +560,7 @@ impl<'a, 'b> Network<'a, 'b> { ) .await; - self + let resp = self .handle_request::<(), ()>(request_props, |_, _| ()) .await; @@ -468,28 +571,30 @@ impl<'a, 'b> Network<'a, 'b> { .data .radarr_data .reset_delete_movie_preferences(); + + resp } - async fn delete_root_folder(&mut self) { - let root_folder_id = self - .app - .lock() - .await - .data - .radarr_data - .root_folders - .current_selection() - .id; + async fn delete_root_folder(&mut self, root_folder_id: Option) -> Result<()> { + let id = if let Some(rf_id) = root_folder_id { + rf_id + } else { + self + .app + .lock() + .await + .data + .radarr_data + .root_folders + .current_selection() + .id + }; - info!("Deleting Radarr root folder for folder with id: {root_folder_id}"); + info!("Deleting Radarr root folder for folder with id: {id}"); let request_props = self .radarr_request_props_from( - format!( - "{}/{root_folder_id}", - RadarrEvent::DeleteRootFolder.resource() - ) - .as_str(), + format!("{}/{id}", RadarrEvent::DeleteRootFolder(None).resource()).as_str(), RequestMethod::Delete, None::<()>, ) @@ -497,87 +602,105 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::<(), ()>(request_props, |_, _| ()) - .await; - } - - async fn download_release(&mut self) { - let (movie_id, _) = self.extract_movie_id().await; - let (guid, title, indexer_id) = { - let app = self.app.lock().await; - let Release { - guid, - title, - indexer_id, - .. - } = app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection(); - - (guid.clone(), title.clone(), *indexer_id) - }; - - info!("Downloading release: {title}"); - - let download_release_body = ReleaseDownloadBody { - guid, - indexer_id, - movie_id, - }; - - let request_props = self - .radarr_request_props_from( - RadarrEvent::DownloadRelease.resource(), - RequestMethod::Post, - Some(download_release_body), - ) - .await; - - self - .handle_request::(request_props, |_, _| ()) - .await; - } - - async fn edit_all_indexer_settings(&mut self) { - info!("Updating Radarr indexer settings"); - - let body = self - .app - .lock() .await - .data - .radarr_data - .indexer_settings - .as_ref() - .unwrap() - .clone(); + } - debug!("Indexer settings body: {body:?}"); + async fn download_release(&mut self, params: Option) -> Result { + let body = if let Some(release_download_body) = params { + info!("Downloading release with params: {release_download_body:?}"); + release_download_body + } else { + let movie_id = self.extract_movie_id().await; + let (guid, title, indexer_id) = { + let app = self.app.lock().await; + let Release { + guid, + title, + indexer_id, + .. + } = app + .data + .radarr_data + .movie_details_modal + .as_ref() + .unwrap() + .movie_releases + .current_selection(); + + (guid.clone(), title.clone(), *indexer_id) + }; + + info!("Downloading release: {title}"); + + ReleaseDownloadBody { + guid, + indexer_id, + movie_id, + } + }; let request_props = self .radarr_request_props_from( - RadarrEvent::EditAllIndexerSettings.resource(), - RequestMethod::Put, + RadarrEvent::DownloadRelease(None).resource(), + RequestMethod::Post, Some(body), ) .await; self + .handle_request::(request_props, |_, _| ()) + .await + } + + async fn edit_all_indexer_settings(&mut self, params: Option) -> Result { + info!("Updating Radarr indexer settings"); + + let body = if let Some(indexer_settings) = params { + indexer_settings + } else { + self + .app + .lock() + .await + .data + .radarr_data + .indexer_settings + .as_ref() + .unwrap() + .clone() + }; + + debug!("Indexer settings body: {body:?}"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::EditAllIndexerSettings(None).resource(), + RequestMethod::Put, + Some(body), + ) + .await; + + let resp = self .handle_request::(request_props, |_, _| {}) .await; self.app.lock().await.data.radarr_data.indexer_settings = None; + + resp } - async fn edit_collection(&mut self) { + async fn edit_collection( + &mut self, + edit_collection_params: Option, + ) -> Result<()> { info!("Editing Radarr collection"); info!("Fetching collection details"); - let collection_id = self.extract_collection_id().await; + let collection_id = if let Some(ref params) = edit_collection_params { + params.collection_id + } else { + self.extract_collection_id().await + }; let request_props = self .radarr_request_props_from( format!("{}/{collection_id}", RadarrEvent::GetCollections.resource()).as_str(), @@ -592,76 +715,127 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), Value>(request_props, |detailed_collection_body, _| { response = detailed_collection_body.to_string() }) - .await; + .await?; info!("Constructing edit collection body"); - let body = { - let mut app = self.app.lock().await; - let mut detailed_collection_body: Value = serde_json::from_str(&response).unwrap(); - let EditCollectionModal { - path, - search_on_add, - minimum_availability_list, - monitored, - quality_profile_list, - } = app.data.radarr_data.edit_collection_modal.as_ref().unwrap(); - let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *app - .data - .radarr_data - .quality_profile_map - .iter() - .filter(|(_, value)| *value == quality_profile) - .map(|(key, _)| key) - .next() - .unwrap(); + let mut detailed_collection_body: Value = serde_json::from_str(&response).unwrap(); + let (monitored, minimum_availability, quality_profile_id, root_folder_path, search_on_add) = + if let Some(params) = edit_collection_params { + let monitored = params.monitored.unwrap_or_else(|| { + detailed_collection_body["monitored"] + .as_bool() + .expect("Unable to deserialize 'monitored' bool") + }); + let minimum_availability = params + .minimum_availability + .unwrap_or_else(|| { + serde_json::from_value(detailed_collection_body["minimumAvailability"].clone()) + .expect("Unable to deserialize 'minimumAvailability'") + }) + .to_string(); + let quality_profile_id = params.quality_profile_id.unwrap_or_else(|| { + detailed_collection_body["qualityProfileId"] + .as_i64() + .expect("Unable to deserialize 'qualityProfileId'") + }); + let root_folder_path = params.root_folder_path.unwrap_or_else(|| { + detailed_collection_body["rootFolderPath"] + .as_str() + .expect("Unable to deserialize 'rootFolderPath'") + .to_owned() + }); + let search_on_add = params.search_on_add.unwrap_or_else(|| { + detailed_collection_body["searchOnAdd"] + .as_bool() + .expect("Unable to deserialize 'searchOnAdd'") + }); - let root_folder_path: String = path.text.clone(); - let monitored = monitored.unwrap_or_default(); - let search_on_add = search_on_add.unwrap_or_default(); - let minimum_availability = minimum_availability_list.current_selection().to_string(); + ( + monitored, + minimum_availability, + quality_profile_id, + root_folder_path, + search_on_add, + ) + } else { + let mut app = self.app.lock().await; + let EditCollectionModal { + path, + search_on_add, + minimum_availability_list, + monitored, + quality_profile_list, + } = app.data.radarr_data.edit_collection_modal.as_ref().unwrap(); + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *app + .data + .radarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); - *detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); - *detailed_collection_body - .get_mut("minimumAvailability") - .unwrap() = json!(minimum_availability); - *detailed_collection_body - .get_mut("qualityProfileId") - .unwrap() = json!(quality_profile_id); - *detailed_collection_body.get_mut("rootFolderPath").unwrap() = json!(root_folder_path); - *detailed_collection_body.get_mut("searchOnAdd").unwrap() = json!(search_on_add); + let root_folder_path: String = path.text.clone(); + let monitored = monitored.unwrap_or_default(); + let search_on_add = search_on_add.unwrap_or_default(); + let minimum_availability = minimum_availability_list.current_selection().to_string(); + app.data.radarr_data.edit_collection_modal = None; - app.data.radarr_data.edit_collection_modal = None; + ( + monitored, + minimum_availability, + quality_profile_id, + root_folder_path, + search_on_add, + ) + }; - detailed_collection_body - }; + *detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_collection_body + .get_mut("minimumAvailability") + .unwrap() = json!(minimum_availability); + *detailed_collection_body + .get_mut("qualityProfileId") + .unwrap() = json!(quality_profile_id); + *detailed_collection_body.get_mut("rootFolderPath").unwrap() = json!(root_folder_path); + *detailed_collection_body.get_mut("searchOnAdd").unwrap() = json!(search_on_add); - debug!("Edit collection body: {body:?}"); + debug!("Edit collection body: {detailed_collection_body:?}"); let request_props = self .radarr_request_props_from( - format!("{}/{collection_id}", RadarrEvent::EditCollection.resource()).as_str(), + format!( + "{}/{collection_id}", + RadarrEvent::EditCollection(None).resource() + ) + .as_str(), RequestMethod::Put, - Some(body), + Some(detailed_collection_body), ) .await; self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn edit_indexer(&mut self) { - let id = self - .app - .lock() - .await - .data - .radarr_data - .indexers - .current_selection() - .id; + async fn edit_indexer(&mut self, edit_indexer_params: Option) -> Result<()> { + let id = if let Some(ref params) = edit_indexer_params { + params.indexer_id + } else { + self + .app + .lock() + .await + .data + .radarr_data + .indexers + .current_selection() + .id + }; info!("Updating Radarr indexer with ID: {id}"); info!("Fetching indexer details for indexer with ID: {id}"); @@ -680,11 +854,116 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), Value>(request_props, |detailed_indexer_body, _| { response = detailed_indexer_body.to_string() }) - .await; + .await?; info!("Constructing edit indexer body"); - let body = { + let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap(); + let priority = detailed_indexer_body["priority"] + .as_i64() + .expect("Unable to deserialize 'priority'"); + + let ( + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + tags, + priority, + ) = if let Some(params) = edit_indexer_params { + let seed_ratio_field_option = detailed_indexer_body["fields"] + .as_array() + .unwrap() + .iter() + .find(|field| field["name"] == "seedCriteria.seedRatio"); + let name = params.name.unwrap_or( + detailed_indexer_body["name"] + .as_str() + .expect("Unable to deserialize 'name'") + .to_owned(), + ); + let enable_rss = params.enable_rss.unwrap_or( + detailed_indexer_body["enableRss"] + .as_bool() + .expect("Unable to deserialize 'enableRss'"), + ); + let enable_automatic_search = params.enable_automatic_search.unwrap_or( + detailed_indexer_body["enableAutomaticSearch"] + .as_bool() + .expect("Unable to deserialize 'enableAutomaticSearch"), + ); + let enable_interactive_search = params.enable_interactive_search.unwrap_or( + detailed_indexer_body["enableInteractiveSearch"] + .as_bool() + .expect("Unable to deserialize 'enableInteractiveSearch'"), + ); + let url = params.url.unwrap_or( + detailed_indexer_body["fields"] + .as_array() + .expect("Unable to deserialize 'fields'") + .iter() + .find(|field| field["name"] == "baseUrl") + .expect("Field 'baseUrl' was not found in the 'fields' array") + .get("value") + .unwrap_or(&json!("")) + .as_str() + .expect("Unable to deserialize 'baseUrl value'") + .to_owned(), + ); + let api_key = params.api_key.unwrap_or( + detailed_indexer_body["fields"] + .as_array() + .expect("Unable to deserialize 'fields'") + .iter() + .find(|field| field["name"] == "apiKey") + .expect("Field 'apiKey' was not found in the 'fields' array") + .get("value") + .unwrap_or(&json!("")) + .as_str() + .expect("Unable to deserialize 'apiKey value'") + .to_owned(), + ); + let seed_ratio = params.seed_ratio.unwrap_or_else(|| { + if let Some(seed_ratio_field) = seed_ratio_field_option { + return seed_ratio_field + .get("value") + .unwrap_or(&json!("")) + .as_str() + .expect("Unable to deserialize 'seedCriteria.seedRatio value'") + .to_owned(); + } + + String::new() + }); + let tags = if params.clear_tags { + vec![] + } else { + params.tags.unwrap_or( + detailed_indexer_body["tags"] + .as_array() + .expect("Unable to deserialize 'tags'") + .iter() + .map(|item| item.as_i64().expect("Unable to deserialize tag ID")) + .collect(), + ) + }; + let priority = params.priority.unwrap_or(priority); + + ( + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + tags, + priority, + ) + } else { let tags = self .app .lock() @@ -699,91 +978,113 @@ impl<'a, 'b> Network<'a, 'b> { .clone(); let tag_ids_vec = self.extract_and_add_tag_ids_vec(tags).await; let mut app = self.app.lock().await; - let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap(); - let EditIndexerModal { - name, - enable_rss, - enable_automatic_search, - enable_interactive_search, - url, - api_key, - seed_ratio, - .. - } = app.data.radarr_data.edit_indexer_modal.as_ref().unwrap(); + let params = { + let EditIndexerModal { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + .. + } = app.data.radarr_data.edit_indexer_modal.as_ref().unwrap(); - *detailed_indexer_body.get_mut("name").unwrap() = json!(name.text.clone()); - *detailed_indexer_body.get_mut("enableRss").unwrap() = json!(enable_rss.unwrap_or_default()); - *detailed_indexer_body - .get_mut("enableAutomaticSearch") - .unwrap() = json!(enable_automatic_search.unwrap_or_default()); - *detailed_indexer_body - .get_mut("enableInteractiveSearch") - .unwrap() = json!(enable_interactive_search.unwrap_or_default()); - *detailed_indexer_body - .get_mut("fields") - .unwrap() - .as_array_mut() - .unwrap() - .iter_mut() - .find(|field| field["name"] == "baseUrl") - .unwrap() - .get_mut("value") - .unwrap() = json!(url.text.clone()); - *detailed_indexer_body - .get_mut("fields") - .unwrap() - .as_array_mut() - .unwrap() - .iter_mut() - .find(|field| field["name"] == "apiKey") - .unwrap() - .get_mut("value") - .unwrap() = json!(api_key.text.clone()); - *detailed_indexer_body.get_mut("tags").unwrap() = json!(tag_ids_vec); - let seed_ratio_field_option = detailed_indexer_body - .get_mut("fields") - .unwrap() - .as_array_mut() - .unwrap() - .iter_mut() - .find(|field| field["name"] == "seedCriteria.seedRatio"); - if let Some(seed_ratio_field) = seed_ratio_field_option { - seed_ratio_field - .as_object_mut() - .unwrap() - .insert("value".to_string(), json!(seed_ratio.text.clone())); - } + ( + name.text.clone(), + enable_rss.unwrap_or_default(), + enable_automatic_search.unwrap_or_default(), + enable_interactive_search.unwrap_or_default(), + url.text.clone(), + api_key.text.clone(), + seed_ratio.text.clone(), + tag_ids_vec, + priority, + ) + }; app.data.radarr_data.edit_indexer_modal = None; - detailed_indexer_body + params }; - debug!("Edit indexer body: {body:?}"); + *detailed_indexer_body.get_mut("name").unwrap() = json!(name); + *detailed_indexer_body.get_mut("priority").unwrap() = json!(priority); + *detailed_indexer_body.get_mut("enableRss").unwrap() = json!(enable_rss); + *detailed_indexer_body + .get_mut("enableAutomaticSearch") + .unwrap() = json!(enable_automatic_search); + *detailed_indexer_body + .get_mut("enableInteractiveSearch") + .unwrap() = json!(enable_interactive_search); + *detailed_indexer_body + .get_mut("fields") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|field| field["name"] == "baseUrl") + .unwrap() + .get_mut("value") + .unwrap() = json!(url); + *detailed_indexer_body + .get_mut("fields") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|field| field["name"] == "apiKey") + .unwrap() + .get_mut("value") + .unwrap() = json!(api_key); + *detailed_indexer_body.get_mut("tags").unwrap() = json!(tags); + let seed_ratio_field_option = detailed_indexer_body + .get_mut("fields") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|field| field["name"] == "seedCriteria.seedRatio"); + if let Some(seed_ratio_field) = seed_ratio_field_option { + seed_ratio_field + .as_object_mut() + .unwrap() + .insert("value".to_string(), json!(seed_ratio)); + } + + debug!("Edit indexer body: {detailed_indexer_body:?}"); let request_props = self .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::EditIndexer.resource()).as_str(), + format!("{}/{id}", RadarrEvent::EditIndexer(None).resource()).as_str(), RequestMethod::Put, - Some(body), + Some(detailed_indexer_body), ) .await; self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn edit_movie(&mut self) { + async fn edit_movie(&mut self, edit_movie_params: Option) -> Result<()> { info!("Editing Radarr movie"); - let (movie_id, tmdb_id) = self.extract_movie_id().await; - info!("Fetching movie details for movie with TMDB ID: {tmdb_id}"); + let movie_id = if let Some(ref params) = edit_movie_params { + params.movie_id + } else { + self.extract_movie_id().await + }; + info!("Fetching movie details for movie with ID: {movie_id}"); let request_props = self .radarr_request_props_from( - format!("{}/{movie_id}", RadarrEvent::GetMovieDetails.resource()).as_str(), + format!( + "{}/{movie_id}", + RadarrEvent::GetMovieDetails(None).resource() + ) + .as_str(), RequestMethod::Get, None::<()>, ) @@ -795,73 +1096,127 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), Value>(request_props, |detailed_movie_body, _| { response = detailed_movie_body.to_string() }) - .await; + .await?; info!("Constructing edit movie body"); - let body = { - let tags = self - .app - .lock() - .await - .data - .radarr_data - .edit_movie_modal - .as_ref() - .unwrap() - .tags - .text - .clone(); - let tag_ids_vec = self.extract_and_add_tag_ids_vec(tags).await; - let mut app = self.app.lock().await; - let mut detailed_movie_body: Value = serde_json::from_str(&response).unwrap(); + let mut detailed_movie_body: Value = serde_json::from_str(&response).unwrap(); + let (monitored, minimum_availability, quality_profile_id, root_folder_path, tags) = + if let Some(params) = edit_movie_params { + let monitored = params.monitored.unwrap_or( + detailed_movie_body["monitored"] + .as_bool() + .expect("Unable to deserialize 'monitored'"), + ); + let minimum_availability = params + .minimum_availability + .unwrap_or_else(|| { + serde_json::from_value(detailed_movie_body["minimumAvailability"].clone()) + .expect("Unable to deserialize 'minimumAvailability'") + }) + .to_string(); + let quality_profile_id = params.quality_profile_id.unwrap_or_else(|| { + detailed_movie_body["qualityProfileId"] + .as_i64() + .expect("Unable to deserialize 'qualityProfileId'") + }); + let root_folder_path = params.root_folder_path.unwrap_or_else(|| { + detailed_movie_body["path"] + .as_str() + .expect("Unable to deserialize 'path'") + .to_owned() + }); + let tags = if params.clear_tags { + vec![] + } else { + params.tags.unwrap_or( + detailed_movie_body["tags"] + .as_array() + .expect("Unable to deserialize 'tags'") + .iter() + .map(|item| item.as_i64().expect("Unable to deserialize tag ID")) + .collect(), + ) + }; - let EditMovieModal { - monitored, - path, - minimum_availability_list, - quality_profile_list, - .. - } = app.data.radarr_data.edit_movie_modal.as_ref().unwrap(); - let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *app - .data - .radarr_data - .quality_profile_map - .iter() - .filter(|(_, value)| *value == quality_profile) - .map(|(key, _)| key) - .next() - .unwrap(); + ( + monitored, + minimum_availability, + quality_profile_id, + root_folder_path, + tags, + ) + } else { + let tags = self + .app + .lock() + .await + .data + .radarr_data + .edit_movie_modal + .as_ref() + .unwrap() + .tags + .text + .clone(); + let tag_ids_vec = self.extract_and_add_tag_ids_vec(tags).await; + let mut app = self.app.lock().await; - *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored.unwrap_or_default()); - *detailed_movie_body.get_mut("minimumAvailability").unwrap() = - json!(minimum_availability_list.current_selection().to_string()); - *detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); - *detailed_movie_body.get_mut("path").unwrap() = json!(path.text.clone()); - *detailed_movie_body.get_mut("tags").unwrap() = json!(tag_ids_vec); + let params = { + let EditMovieModal { + monitored, + path, + minimum_availability_list, + quality_profile_list, + .. + } = app.data.radarr_data.edit_movie_modal.as_ref().unwrap(); + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *app + .data + .radarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); - app.data.radarr_data.edit_movie_modal = None; + ( + monitored.unwrap_or_default(), + minimum_availability_list.current_selection().to_string(), + quality_profile_id, + path.text.clone(), + tag_ids_vec, + ) + }; - detailed_movie_body - }; + app.data.radarr_data.edit_movie_modal = None; - debug!("Edit movie body: {body:?}"); + params + }; + + *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability); + *detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); + *detailed_movie_body.get_mut("path").unwrap() = json!(root_folder_path); + *detailed_movie_body.get_mut("tags").unwrap() = json!(tags); + + debug!("Edit movie body: {detailed_movie_body:?}"); let request_props = self .radarr_request_props_from( - format!("{}/{movie_id}", RadarrEvent::EditMovie.resource()).as_str(), + format!("{}/{movie_id}", RadarrEvent::EditMovie(None).resource()).as_str(), RequestMethod::Put, - Some(body), + Some(detailed_movie_body), ) .await; self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn get_blocklist(&mut self) { + async fn get_blocklist(&mut self) -> Result { info!("Fetching blocklist"); let request_props = self @@ -884,10 +1239,10 @@ impl<'a, 'b> Network<'a, 'b> { app.data.radarr_data.blocklist.apply_sorting_toggle(false); } }) - .await; + .await } - async fn get_collections(&mut self) { + async fn get_collections(&mut self) -> Result> { info!("Fetching Radarr collections"); let request_props = self @@ -909,14 +1264,14 @@ impl<'a, 'b> Network<'a, 'b> { app.data.radarr_data.collections.apply_sorting_toggle(false); } }) - .await; + .await } - async fn get_credits(&mut self) { + async fn get_credits(&mut self, movie_id: Option) -> Result> { info!("Fetching Radarr movie credits"); let request_uri = self - .append_movie_id_param(RadarrEvent::GetMovieCredits.resource()) + .append_movie_id_param(RadarrEvent::GetMovieCredits(None).resource(), movie_id) .await; let request_props = self .radarr_request_props_from(request_uri.as_str(), RequestMethod::Get, None::<()>) @@ -956,10 +1311,10 @@ impl<'a, 'b> Network<'a, 'b> { .movie_crew .set_items(crew_vec); }) - .await; + .await } - async fn get_diskspace(&mut self) { + async fn get_diskspace(&mut self) -> Result> { info!("Fetching Radarr disk space"); let request_props = self @@ -974,10 +1329,10 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), Vec>(request_props, |disk_space_vec, mut app| { app.data.radarr_data.disk_space_vec = disk_space_vec; }) - .await; + .await } - async fn get_downloads(&mut self) { + async fn get_downloads(&mut self) -> Result { info!("Fetching Radarr downloads"); let request_props = self @@ -999,7 +1354,7 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_indexers(&mut self) { + async fn get_indexers(&mut self) -> Result> { info!("Fetching Radarr indexers"); let request_props = self @@ -1017,12 +1372,12 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_indexer_settings(&mut self) { + async fn get_all_indexer_settings(&mut self) -> Result { info!("Fetching Radarr indexer settings"); let request_props = self .radarr_request_props_from( - RadarrEvent::GetIndexerSettings.resource(), + RadarrEvent::GetAllIndexerSettings.resource(), RequestMethod::Get, None::<()>, ) @@ -1036,10 +1391,10 @@ impl<'a, 'b> Network<'a, 'b> { debug!("Indexer Settings are being modified. Ignoring update..."); } }) - .await; + .await } - async fn get_healthcheck(&mut self) { + async fn get_healthcheck(&mut self) -> Result<()> { info!("Performing Radarr health check"); let request_props = self @@ -1052,15 +1407,16 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::<(), ()>(request_props, |_, _| ()) - .await; + .await } - async fn get_logs(&mut self) { + async fn get_logs(&mut self, events: Option) -> Result { info!("Fetching Radarr logs"); let resource = format!( - "{}?pageSize=500&sortDirection=descending&sortKey=time", - RadarrEvent::GetLogs.resource() + "{}?pageSize={}&sortDirection=descending&sortKey=time", + RadarrEvent::GetLogs(events).resource(), + events.unwrap_or(500) ); let request_props = self .radarr_request_props_from(&resource, RequestMethod::Get, None::<()>) @@ -1098,18 +1454,22 @@ impl<'a, 'b> Network<'a, 'b> { app.data.radarr_data.logs.set_items(log_lines); app.data.radarr_data.logs.scroll_to_bottom(); }) - .await; + .await } - async fn get_movie_details(&mut self) { + async fn get_movie_details(&mut self, movie_id: Option) -> Result { info!("Fetching Radarr movie details"); - let (movie_id, tmdb_id) = self.extract_movie_id().await; - info!("Fetching movie details for movie with TMDB ID: {tmdb_id}"); + let id = if let Some(m_id) = movie_id { + m_id + } else { + self.extract_movie_id().await + }; + info!("Fetching movie details for movie with ID: {id}"); let request_props = self .radarr_request_props_from( - format!("{}/{movie_id}", RadarrEvent::GetMovieDetails.resource()).as_str(), + format!("{}/{id}", RadarrEvent::GetMovieDetails(None).resource()).as_str(), RequestMethod::Get, None::<()>, ) @@ -1142,7 +1502,7 @@ impl<'a, 'b> Network<'a, 'b> { .radarr_data .quality_profile_map .get_by_left(&quality_profile_id) - .unwrap() + .unwrap_or(&"".to_owned()) .to_owned(); let imdb_rating = if let Some(rating) = ratings.imdb { if let Some(value) = rating.value.as_f64() { @@ -1251,14 +1611,14 @@ impl<'a, 'b> Network<'a, 'b> { app.data.radarr_data.movie_details_modal = Some(movie_details_modal); }) - .await; + .await } - async fn get_movie_history(&mut self) { + async fn get_movie_history(&mut self, movie_id: Option) -> Result> { info!("Fetching Radarr movie history"); let request_uri = self - .append_movie_id_param(RadarrEvent::GetMovieHistory.resource()) + .append_movie_id_param(RadarrEvent::GetMovieHistory(None).resource(), movie_id) .await; let request_props = self .radarr_request_props_from(request_uri.as_str(), RequestMethod::Get, None::<()>) @@ -1282,10 +1642,10 @@ impl<'a, 'b> Network<'a, 'b> { .movie_history .set_items(reversed_movie_history_vec) }) - .await; + .await } - async fn get_movies(&mut self) { + async fn get_movies(&mut self) -> Result> { info!("Fetching Radarr library"); let request_props = self @@ -1307,10 +1667,10 @@ impl<'a, 'b> Network<'a, 'b> { app.data.radarr_data.movies.apply_sorting_toggle(false); } }) - .await; + .await } - async fn get_quality_profiles(&mut self) { + async fn get_quality_profiles(&mut self) -> Result> { info!("Fetching Radarr quality profiles"); let request_props = self @@ -1328,10 +1688,10 @@ impl<'a, 'b> Network<'a, 'b> { .map(|profile| (profile.id, profile.name)) .collect(); }) - .await; + .await } - async fn get_queued_events(&mut self) { + async fn get_queued_events(&mut self) -> Result> { info!("Fetching Radarr queued events"); let request_props = self @@ -1350,16 +1710,20 @@ impl<'a, 'b> Network<'a, 'b> { .queued_events .set_items(queued_events_vec); }) - .await; + .await } - async fn get_releases(&mut self) { - let (movie_id, tmdb_id) = self.extract_movie_id().await; - info!("Fetching releases for movie with TMDB id {tmdb_id} and with Radarr id: {movie_id}"); + async fn get_releases(&mut self, movie_id: Option) -> Result> { + let id = if let Some(m_id) = movie_id { + m_id + } else { + self.extract_movie_id().await + }; + info!("Fetching releases for movie with ID: {id}"); let request_props = self .radarr_request_props_from( - format!("{}?movieId={movie_id}", RadarrEvent::GetReleases.resource()).as_str(), + format!("{}?movieId={id}", RadarrEvent::GetReleases(None).resource()).as_str(), RequestMethod::Get, None::<()>, ) @@ -1380,10 +1744,10 @@ impl<'a, 'b> Network<'a, 'b> { .movie_releases .set_items(release_vec); }) - .await; + .await } - async fn get_root_folders(&mut self) { + async fn get_root_folders(&mut self) -> Result> { info!("Fetching Radarr root folders"); let request_props = self @@ -1398,10 +1762,10 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), Vec>(request_props, |root_folders, mut app| { app.data.radarr_data.root_folders.set_items(root_folders); }) - .await; + .await } - async fn get_status(&mut self) { + async fn get_status(&mut self) -> Result { info!("Fetching Radarr system status"); let request_props = self @@ -1417,10 +1781,10 @@ impl<'a, 'b> Network<'a, 'b> { app.data.radarr_data.version = system_status.version; app.data.radarr_data.start_time = system_status.start_time; }) - .await; + .await } - async fn get_tags(&mut self) { + async fn get_tags(&mut self) -> Result> { info!("Fetching Radarr tags"); let request_props = self @@ -1438,10 +1802,10 @@ impl<'a, 'b> Network<'a, 'b> { .map(|tag| (tag.id, tag.label)) .collect(); }) - .await; + .await } - async fn get_tasks(&mut self) { + async fn get_tasks(&mut self) -> Result> { info!("Fetching Radarr tasks"); let request_props = self @@ -1456,10 +1820,10 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), Vec>(request_props, |tasks_vec, mut app| { app.data.radarr_data.tasks.set_items(tasks_vec); }) - .await; + .await } - async fn get_updates(&mut self) { + async fn get_updates(&mut self) -> Result> { info!("Fetching Radarr updates"); let request_props = self @@ -1537,20 +1901,24 @@ impl<'a, 'b> Network<'a, 'b> { {updates}" )); }) - .await; + .await } - async fn search_movie(&mut self) { + async fn search_movie(&mut self, query: Option) -> Result> { info!("Searching for specific Radarr movie"); - let search = self - .app - .lock() - .await - .data - .radarr_data - .add_movie_search - .clone() - .ok_or(anyhow!("Encountered a race condition")); + let search = if let Some(search_query) = query { + Ok(search_query.into()) + } else { + self + .app + .lock() + .await + .data + .radarr_data + .add_movie_search + .clone() + .ok_or(anyhow!("Encountered a race condition")) + }; match search { Ok(search_string) => { @@ -1558,7 +1926,7 @@ impl<'a, 'b> Network<'a, 'b> { .radarr_request_props_from( format!( "{}?term={}", - RadarrEvent::SearchNewMovie.resource(), + RadarrEvent::SearchNewMovie(None).resource(), encode(&search_string.text) ) .as_str(), @@ -1583,7 +1951,7 @@ impl<'a, 'b> Network<'a, 'b> { app.data.radarr_data.add_searched_movies = Some(add_searched_movies); } }) - .await; + .await } Err(e) => { warn!( @@ -1591,21 +1959,26 @@ impl<'a, 'b> Network<'a, 'b> { This is most likely caused by the user trying to navigate between modals rapidly. \ Ignoring search request." ); + Ok(Vec::default()) } } } - async fn start_task(&mut self) { - let task_name = self - .app - .lock() - .await - .data - .radarr_data - .tasks - .current_selection() - .task_name - .clone(); + async fn start_task(&mut self, task: Option) -> Result { + let task_name = if let Some(t_name) = task { + t_name + } else { + self + .app + .lock() + .await + .data + .radarr_data + .tasks + .current_selection() + .task_name + } + .to_string(); info!("Starting Radarr task: {task_name}"); @@ -1613,7 +1986,7 @@ impl<'a, 'b> Network<'a, 'b> { let request_props = self .radarr_request_props_from( - RadarrEvent::StartTask.resource(), + RadarrEvent::StartTask(None).resource(), RequestMethod::Post, Some(body), ) @@ -1621,19 +1994,23 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn test_indexer(&mut self) { - let id = self - .app - .lock() - .await - .data - .radarr_data - .indexers - .current_selection() - .id; + async fn test_indexer(&mut self, indexer_id: Option) -> Result { + let id = if let Some(i_id) = indexer_id { + i_id + } else { + self + .app + .lock() + .await + .data + .radarr_data + .indexers + .current_selection() + .id + }; info!("Testing Radarr indexer with ID: {id}"); info!("Fetching indexer details for indexer with ID: {id}"); @@ -1652,13 +2029,13 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), Value>(request_props, |detailed_indexer_body, _| { test_body = detailed_indexer_body; }) - .await; + .await?; info!("Testing indexer"); let mut request_props = self .radarr_request_props_from( - RadarrEvent::TestIndexer.resource(), + RadarrEvent::TestIndexer(None).resource(), RequestMethod::Post, Some(test_body), ) @@ -1676,10 +2053,10 @@ impl<'a, 'b> Network<'a, 'b> { ); }; }) - .await; + .await } - async fn test_all_indexers(&mut self) { + async fn test_all_indexers(&mut self) -> Result> { info!("Testing all indexers"); let mut request_props = self @@ -1726,20 +2103,24 @@ impl<'a, 'b> Network<'a, 'b> { test_all_indexer_results.set_items(modal_test_results); app.data.radarr_data.indexer_test_all_results = Some(test_all_indexer_results); }) - .await; + .await } - async fn trigger_automatic_search(&mut self) { - let (movie_id, tmdb_id) = self.extract_movie_id().await; - info!("Searching indexers for movie with TMDB id {tmdb_id} and with Radarr id: {movie_id}"); + async fn trigger_automatic_search(&mut self, movie_id: Option) -> Result { + let id = if let Some(m_id) = movie_id { + m_id + } else { + self.extract_movie_id().await + }; + info!("Searching indexers for movie with ID: {id}"); let body = MovieCommandBody { name: "MoviesSearch".to_owned(), - movie_ids: vec![movie_id], + movie_ids: vec![id], }; let request_props = self .radarr_request_props_from( - RadarrEvent::TriggerAutomaticSearch.resource(), + RadarrEvent::TriggerAutomaticSearch(None).resource(), RequestMethod::Post, Some(body), ) @@ -1747,10 +2128,10 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn update_all_movies(&mut self) { + async fn update_all_movies(&mut self) -> Result { info!("Updating all movies"); let body = MovieCommandBody { name: "RefreshMovie".to_owned(), @@ -1767,20 +2148,24 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn update_and_scan(&mut self) { - let (movie_id, tmdb_id) = self.extract_movie_id().await; - info!("Updating and scanning movie with TMDB id {tmdb_id} and with Radarr id: {movie_id}"); + async fn update_and_scan(&mut self, movie_id: Option) -> Result { + let id = if let Some(m_id) = movie_id { + m_id + } else { + self.extract_movie_id().await + }; + info!("Updating and scanning movie with ID: {id}"); let body = MovieCommandBody { name: "RefreshMovie".to_owned(), - movie_ids: vec![movie_id], + movie_ids: vec![id], }; let request_props = self .radarr_request_props_from( - RadarrEvent::UpdateAndScan.resource(), + RadarrEvent::UpdateAndScan(None).resource(), RequestMethod::Post, Some(body), ) @@ -1788,10 +2173,10 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn update_collections(&mut self) { + async fn update_collections(&mut self) -> Result { info!("Updating collections"); let body = CommandBody { name: "RefreshCollections".to_owned(), @@ -1807,10 +2192,10 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } - async fn update_downloads(&mut self) { + async fn update_downloads(&mut self) -> Result { info!("Updating downloads"); let body = CommandBody { name: "RefreshMonitoredDownloads".to_owned(), @@ -1826,7 +2211,7 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |_, _| ()) - .await; + .await } async fn radarr_request_props_from( @@ -1861,7 +2246,10 @@ impl<'a, 'b> Network<'a, 'b> { .collect::>(); for tag in missing_tags_vec { - self.add_tag(tag.trim().to_owned()).await; + self + .add_tag(tag.trim().to_owned()) + .await + .expect("Unable to add tag"); } let app = self.app.lock().await; @@ -1879,12 +2267,16 @@ impl<'a, 'b> Network<'a, 'b> { .collect() } - async fn extract_movie_id(&mut self) -> (i64, i64) { - let app = self.app.lock().await; - ( - app.data.radarr_data.movies.current_selection().id, - app.data.radarr_data.movies.current_selection().tmdb_id, - ) + async fn extract_movie_id(&mut self) -> i64 { + self + .app + .lock() + .await + .data + .radarr_data + .movies + .current_selection() + .id } async fn extract_collection_id(&mut self) -> i64 { @@ -1899,8 +2291,12 @@ impl<'a, 'b> Network<'a, 'b> { .id } - async fn append_movie_id_param(&mut self, resource: &str) -> String { - let (movie_id, _) = self.extract_movie_id().await; + async fn append_movie_id_param(&mut self, resource: &str, movie_id: Option) -> String { + let movie_id = if let Some(id) = movie_id { + id + } else { + self.extract_movie_id().await + }; format!("{resource}?movieId={movie_id}") } } diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index eea3b1c..c8e665f 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -111,11 +111,11 @@ mod test { #[rstest] fn test_resource_movie( #[values( - RadarrEvent::AddMovie, - RadarrEvent::EditMovie, + RadarrEvent::AddMovie(None), + RadarrEvent::EditMovie(None), RadarrEvent::GetMovies, - RadarrEvent::GetMovieDetails, - RadarrEvent::DeleteMovie + RadarrEvent::GetMovieDetails(None), + RadarrEvent::DeleteMovie(None) )] event: RadarrEvent, ) { @@ -124,21 +124,24 @@ mod test { #[rstest] fn test_resource_collection( - #[values(RadarrEvent::GetCollections, RadarrEvent::EditCollection)] event: RadarrEvent, + #[values(RadarrEvent::GetCollections, RadarrEvent::EditCollection(None))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/collection"); } #[rstest] fn test_resource_indexer( - #[values(RadarrEvent::GetIndexers, RadarrEvent::DeleteIndexer)] event: RadarrEvent, + #[values(RadarrEvent::GetIndexers, RadarrEvent::DeleteIndexer(None))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/indexer"); } #[rstest] - fn test_resource_indexer_settings( - #[values(RadarrEvent::GetIndexerSettings, RadarrEvent::EditAllIndexerSettings)] + fn test_resource_all_indexer_settings( + #[values( + RadarrEvent::GetAllIndexerSettings, + RadarrEvent::EditAllIndexerSettings(None) + )] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/config/indexer"); @@ -147,9 +150,9 @@ mod test { #[rstest] fn test_resource_root_folder( #[values( - RadarrEvent::AddRootFolder, + RadarrEvent::AddRootFolder(None), RadarrEvent::GetRootFolders, - RadarrEvent::DeleteRootFolder + RadarrEvent::DeleteRootFolder(None) )] event: RadarrEvent, ) { @@ -158,14 +161,15 @@ mod test { #[rstest] fn test_resource_release( - #[values(RadarrEvent::GetReleases, RadarrEvent::DownloadRelease)] event: RadarrEvent, + #[values(RadarrEvent::GetReleases(None), RadarrEvent::DownloadRelease(None))] + event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/release"); } #[rstest] fn test_resource_queue( - #[values(RadarrEvent::GetDownloads, RadarrEvent::DeleteDownload)] event: RadarrEvent, + #[values(RadarrEvent::GetDownloads, RadarrEvent::DeleteDownload(None))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/queue"); } @@ -173,10 +177,10 @@ mod test { #[rstest] fn test_resource_command( #[values( - RadarrEvent::StartTask, + RadarrEvent::StartTask(None), RadarrEvent::GetQueuedEvents, - RadarrEvent::TriggerAutomaticSearch, - RadarrEvent::UpdateAndScan, + RadarrEvent::TriggerAutomaticSearch(None), + RadarrEvent::UpdateAndScan(None), RadarrEvent::UpdateAllMovies, RadarrEvent::UpdateDownloads, RadarrEvent::UpdateCollections @@ -188,19 +192,19 @@ mod test { #[rstest] #[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")] - #[case(RadarrEvent::DeleteBlocklistItem, "/blocklist")] + #[case(RadarrEvent::DeleteBlocklistItem(None), "/blocklist")] #[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] - #[case(RadarrEvent::GetLogs, "/log")] - #[case(RadarrEvent::SearchNewMovie, "/movie/lookup")] - #[case(RadarrEvent::GetMovieCredits, "/credit")] - #[case(RadarrEvent::GetMovieHistory, "/history/movie")] + #[case(RadarrEvent::GetLogs(Some(500)), "/log")] + #[case(RadarrEvent::SearchNewMovie(None), "/movie/lookup")] + #[case(RadarrEvent::GetMovieCredits(None), "/credit")] + #[case(RadarrEvent::GetMovieHistory(None), "/history/movie")] #[case(RadarrEvent::GetOverview, "/diskspace")] #[case(RadarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(RadarrEvent::GetStatus, "/system/status")] #[case(RadarrEvent::GetTags, "/tag")] #[case(RadarrEvent::GetTasks, "/system/task")] #[case(RadarrEvent::GetUpdates, "/update")] - #[case(RadarrEvent::TestIndexer, "/indexer/test")] + #[case(RadarrEvent::TestIndexer(None), "/indexer/test")] #[case(RadarrEvent::TestAllIndexers, "/indexer/testall")] #[case(RadarrEvent::HealthCheck, "/health")] fn test_resource(#[case] event: RadarrEvent, #[case] expected_uri: String) { @@ -227,7 +231,7 @@ mod test { .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::HealthCheck).await; + let _ = network.handle_radarr_event(RadarrEvent::HealthCheck).await; async_server.assert_async().await; } @@ -252,23 +256,29 @@ mod test { ) .await; let mut network = Network::new(&app_arc, CancellationToken::new()); + let disk_space_vec = vec![ + DiskSpace { + free_space: 1111, + total_space: 2222, + }, + DiskSpace { + free_space: 3333, + total_space: 4444, + }, + ]; - network.handle_radarr_event(RadarrEvent::GetOverview).await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.disk_space_vec, - vec![ - DiskSpace { - free_space: 1111, - total_space: 2222, - }, - DiskSpace { - free_space: 3333, - total_space: 4444, - }, - ] - ); + if let RadarrSerdeable::DiskSpaces(disk_space) = network + .handle_radarr_event(RadarrEvent::GetOverview) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.disk_space_vec, + disk_space_vec + ); + assert_eq!(disk_space, disk_space_vec); + } } #[tokio::test] @@ -285,16 +295,25 @@ mod test { ) .await; let mut network = Network::new(&app_arc, CancellationToken::new()); + let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()) + as DateTime; - network.handle_radarr_event(RadarrEvent::GetStatus).await; - - async_server.assert_async().await; - assert_str_eq!(app_arc.lock().await.data.radarr_data.version, "v1"); - assert_eq!( - app_arc.lock().await.data.radarr_data.start_time, - DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()) - as DateTime - ); + if let RadarrSerdeable::SystemStatus(status) = network + .handle_radarr_event(RadarrEvent::GetStatus) + .await + .unwrap() + { + async_server.assert_async().await; + assert_str_eq!(app_arc.lock().await.data.radarr_data.version, "v1"); + assert_eq!(app_arc.lock().await.data.radarr_data.start_time, date_time); + assert_eq!( + status, + SystemStatus { + version: "v1".to_owned(), + start_time: date_time + } + ); + } } #[rstest] @@ -306,7 +325,19 @@ mod test { *movie_1.get_mut("title").unwrap() = json!("z test"); *movie_2.get_mut("id").unwrap() = json!(2); *movie_2.get_mut("title").unwrap() = json!("A test"); - let mut expected_movies = vec![ + let expected_movies = vec![ + Movie { + id: 1, + title: "z test".into(), + ..movie() + }, + Movie { + id: 2, + title: "A test".into(), + ..movie() + }, + ]; + let mut expected_sorted_movies = vec![ Movie { id: 1, title: "z test".into(), @@ -334,7 +365,7 @@ mod test { .to_lowercase() .cmp(&b.title.text.to_lowercase()) }; - expected_movies.sort_by(cmp_fn); + expected_sorted_movies.sort_by(cmp_fn); let title_sort_option = SortOption { name: "Title", cmp_fn: Some(cmp_fn), @@ -349,14 +380,19 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetMovies).await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.movies.items, - expected_movies - ); - assert!(app_arc.lock().await.data.radarr_data.movies.sort_asc); + if let RadarrSerdeable::Movies(movies) = network + .handle_radarr_event(RadarrEvent::GetMovies) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.movies.items, + expected_sorted_movies + ); + assert!(app_arc.lock().await.data.radarr_data.movies.sort_asc); + assert_eq!(movies, expected_movies); + } } #[tokio::test] @@ -399,7 +435,10 @@ mod test { .sorting(vec![title_sort_option]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetMovies).await; + assert!(network + .handle_radarr_event(RadarrEvent::GetMovies) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -430,7 +469,7 @@ mod test { "languages": [ { "name": "English" } ], "quality": { "quality": { "name": "HD - 1080p" }} }]); - let resource = format!("{}?movieId=1", RadarrEvent::GetReleases.resource()); + let resource = format!("{}?movieId=1", RadarrEvent::GetReleases(None).resource()); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -449,22 +488,27 @@ mod test { app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetReleases).await; - - async_server.assert_async().await; - assert_eq!( - app_arc - .lock() - .await - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .items, - vec![release()] - ); + if let RadarrSerdeable::Releases(releases_vec) = network + .handle_radarr_event(RadarrEvent::GetReleases(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .radarr_data + .movie_details_modal + .as_ref() + .unwrap() + .movie_releases + .items, + vec![release()] + ); + assert_eq!(releases_vec, vec![release()]); + } } #[tokio::test] @@ -484,7 +528,7 @@ mod test { "languages": [ { "name": "English" } ], "quality": { "quality": { "name": "HD - 1080p" }} }]); - let resource = format!("{}?movieId=1", RadarrEvent::GetReleases.resource()); + let resource = format!("{}?movieId=1", RadarrEvent::GetReleases(None).resource()); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -502,7 +546,10 @@ mod test { .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetReleases).await; + assert!(network + .handle_radarr_event(RadarrEvent::GetReleases(None)) + .await + .is_ok()); async_server.assert_async().await; assert_eq!( @@ -545,7 +592,7 @@ mod test { }]); let resource = format!( "{}?term=test%20term", - RadarrEvent::SearchNewMovie.resource() + RadarrEvent::SearchNewMovie(None).resource() ); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, @@ -558,42 +605,93 @@ mod test { app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::SearchNewMovie) - .await; - - async_server.assert_async().await; - assert!(app_arc - .lock() + if let RadarrSerdeable::AddMovieSearchResults(add_movie_search_results) = network + .handle_radarr_event(RadarrEvent::SearchNewMovie(None)) .await - .data - .radarr_data - .add_searched_movies - .is_some()); - assert_eq!( - app_arc + .unwrap() + { + async_server.assert_async().await; + assert!(app_arc .lock() .await .data .radarr_data .add_searched_movies - .as_ref() - .unwrap() - .items, - vec![add_movie_search_result()] + .is_some()); + assert_eq!( + app_arc + .lock() + .await + .data + .radarr_data + .add_searched_movies + .as_ref() + .unwrap() + .items, + vec![add_movie_search_result()] + ); + assert_eq!(add_movie_search_results, vec![add_movie_search_result()]); + } + } + + #[tokio::test] + async fn test_handle_search_new_movie_event_uses_provided_query() { + let add_movie_search_result_json = json!([{ + "tmdbId": 1234, + "title": "Test", + "originalLanguage": { "name": "English" }, + "status": "released", + "overview": "New movie blah blah blah", + "genres": ["cool", "family", "fun"], + "year": 2023, + "runtime": 120, + "ratings": { + "imdb": { + "value": 9.9 + }, + "tmdb": { + "value": 9.9 + }, + "rottenTomatoes": { + "value": 9.9 + } + } + }]); + let resource = format!( + "{}?term=test%20term", + RadarrEvent::SearchNewMovie(None).resource() ); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(add_movie_search_result_json), + None, + &resource, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + if let RadarrSerdeable::AddMovieSearchResults(add_movie_search_results) = network + .handle_radarr_event(RadarrEvent::SearchNewMovie(Some("test term".into()))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!(add_movie_search_results, vec![add_movie_search_result()]); + } } #[tokio::test] async fn test_handle_start_task_event() { + let response = json!({ "test": "test"}); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Post, Some(json!({ - "name": "TestTask" + "name": "ApplicationCheckUpdate" })), + Some(response.clone()), None, - None, - RadarrEvent::StartTask.resource(), + RadarrEvent::StartTask(None).resource(), ) .await; app_arc @@ -603,30 +701,61 @@ mod test { .radarr_data .tasks .set_items(vec![Task { - task_name: "TestTask".to_owned(), + task_name: TaskName::default(), ..Task::default() }]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::StartTask).await; + if let RadarrSerdeable::Value(value) = network + .handle_radarr_event(RadarrEvent::StartTask(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!(value, response); + } + } - async_server.assert_async().await; + #[tokio::test] + async fn test_handle_start_task_event_uses_provided_task_name() { + let response = json!({ "test": "test"}); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Post, + Some(json!({ + "name": "ApplicationCheckUpdate" + })), + Some(response.clone()), + None, + RadarrEvent::StartTask(None).resource(), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + if let RadarrSerdeable::Value(value) = network + .handle_radarr_event(RadarrEvent::StartTask(Some(TaskName::default()))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!(value, response); + } } #[tokio::test] async fn test_handle_search_new_movie_event_no_results() { let resource = format!( "{}?term=test%20term", - RadarrEvent::SearchNewMovie.resource() + RadarrEvent::SearchNewMovie(None).resource() ); let (async_server, app_arc, _server) = mock_radarr_api(RequestMethod::Get, None, Some(json!([])), None, &resource).await; app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::SearchNewMovie) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::SearchNewMovie(None)) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -646,7 +775,7 @@ mod test { async fn test_handle_search_new_movie_event_no_panic_on_race_condition() { let resource = format!( "{}?term=test%20term", - RadarrEvent::SearchNewMovie.resource() + RadarrEvent::SearchNewMovie(None).resource() ); let mut server = Server::new_async().await; let mut async_server = server @@ -673,9 +802,10 @@ mod test { let app_arc = Arc::new(Mutex::new(app)); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::SearchNewMovie) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::SearchNewMovie(None)) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -734,11 +864,11 @@ mod test { let async_test_server = server .mock( "POST", - format!("/api/v3{}", RadarrEvent::TestIndexer.resource()).as_str(), + format!("/api/v3{}", RadarrEvent::TestIndexer(None).resource()).as_str(), ) .with_status(400) .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(indexer_details_json)) + .match_body(Matcher::Json(indexer_details_json.clone())) .with_body(response_json.to_string()) .create_async() .await; @@ -751,14 +881,19 @@ mod test { .set_items(vec![indexer()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::TestIndexer).await; - - async_details_server.assert_async().await; - async_test_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.indexer_test_error, - Some("\"test failure\"".to_owned()) - ); + if let RadarrSerdeable::Value(value) = network + .handle_radarr_event(RadarrEvent::TestIndexer(None)) + .await + .unwrap() + { + async_details_server.assert_async().await; + async_test_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.indexer_test_error, + Some("\"test failure\"".to_owned()) + ); + assert_eq!(value, response_json) + } } #[tokio::test] @@ -797,11 +932,11 @@ mod test { let async_test_server = server .mock( "POST", - format!("/api/v3{}", RadarrEvent::TestIndexer.resource()).as_str(), + format!("/api/v3{}", RadarrEvent::TestIndexer(None).resource()).as_str(), ) .with_status(200) .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(indexer_details_json)) + .match_body(Matcher::Json(indexer_details_json.clone())) .with_body("{}") .create_async() .await; @@ -814,14 +949,76 @@ mod test { .set_items(vec![indexer()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::TestIndexer).await; + if let RadarrSerdeable::Value(value) = network + .handle_radarr_event(RadarrEvent::TestIndexer(None)) + .await + .unwrap() + { + async_details_server.assert_async().await; + async_test_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.indexer_test_error, + None + ); + assert_eq!(value, json!({})); + } + } - async_details_server.assert_async().await; - async_test_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.indexer_test_error, - None - ); + #[tokio::test] + async fn test_handle_test_indexer_event_success_uses_provided_id() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json.clone()), + None, + &resource, + ) + .await; + let async_test_server = server + .mock( + "POST", + format!("/api/v3{}", RadarrEvent::TestIndexer(None).resource()).as_str(), + ) + .with_status(200) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(indexer_details_json.clone())) + .with_body("{}") + .create_async() + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + if let RadarrSerdeable::Value(value) = network + .handle_radarr_event(RadarrEvent::TestIndexer(Some(1))) + .await + .unwrap() + { + async_details_server.assert_async().await; + async_test_server.assert_async().await; + assert_eq!(value, json!({})); + } } #[tokio::test] @@ -872,6 +1069,7 @@ mod test { }, ] }]); + let response: Vec = serde_json::from_value(response_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Post, None, @@ -889,30 +1087,33 @@ mod test { .set_items(indexers); let mut network = Network::new(&app_arc, CancellationToken::new()); - network + if let RadarrSerdeable::IndexerTestResults(results) = network .handle_radarr_event(RadarrEvent::TestAllIndexers) - .await; - - async_server.assert_async().await; - assert!(app_arc - .lock() .await - .data - .radarr_data - .indexer_test_all_results - .is_some()); - assert_eq!( - app_arc + .unwrap() + { + async_server.assert_async().await; + assert!(app_arc .lock() .await .data .radarr_data .indexer_test_all_results - .as_ref() - .unwrap() - .items, - indexer_test_results_modal_items - ); + .is_some()); + assert_eq!( + app_arc + .lock() + .await + .data + .radarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + .items, + indexer_test_results_modal_items + ); + assert_eq!(results, response); + } } #[tokio::test] @@ -923,9 +1124,9 @@ mod test { "name": "MoviesSearch", "movieIds": [ 1 ] })), + Some(json!({})), None, - None, - RadarrEvent::TriggerAutomaticSearch.resource(), + RadarrEvent::TriggerAutomaticSearch(None).resource(), ) .await; app_arc @@ -937,9 +1138,33 @@ mod test { .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::TriggerAutomaticSearch) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_trigger_automatic_search_event_uses_provided_id() { + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Post, + Some(json!({ + "name": "MoviesSearch", + "movieIds": [ 1 ] + })), + Some(json!({})), + None, + RadarrEvent::TriggerAutomaticSearch(None).resource(), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(Some(1))) + .await + .is_ok()); async_server.assert_async().await; } @@ -952,9 +1177,9 @@ mod test { "name": "RefreshMovie", "movieIds": [ 1 ] })), + Some(json!({})), None, - None, - RadarrEvent::UpdateAndScan.resource(), + RadarrEvent::UpdateAndScan(None).resource(), ) .await; app_arc @@ -966,9 +1191,33 @@ mod test { .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::UpdateAndScan) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::UpdateAndScan(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_update_and_scan_event_uses_provied_movie_id() { + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Post, + Some(json!({ + "name": "RefreshMovie", + "movieIds": [ 1 ] + })), + Some(json!({})), + None, + RadarrEvent::UpdateAndScan(None).resource(), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::UpdateAndScan(Some(1))) + .await + .is_ok()); async_server.assert_async().await; } @@ -981,16 +1230,17 @@ mod test { "name": "RefreshMovie", "movieIds": [] })), - None, + Some(json!({})), None, RadarrEvent::UpdateAllMovies.resource(), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network + assert!(network .handle_radarr_event(RadarrEvent::UpdateAllMovies) - .await; + .await + .is_ok()); async_server.assert_async().await; } @@ -1002,16 +1252,17 @@ mod test { Some(json!({ "name": "RefreshMonitoredDownloads" })), - None, + Some(json!({})), None, RadarrEvent::UpdateDownloads.resource(), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network + assert!(network .handle_radarr_event(RadarrEvent::UpdateDownloads) - .await; + .await + .is_ok()); async_server.assert_async().await; } @@ -1023,23 +1274,25 @@ mod test { Some(json!({ "name": "RefreshCollections" })), - None, + Some(json!({})), None, RadarrEvent::UpdateCollections.resource(), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network + assert!(network .handle_radarr_event(RadarrEvent::UpdateCollections) - .await; + .await + .is_ok()); async_server.assert_async().await; } #[tokio::test] async fn test_handle_get_movie_details_event() { - let resource = format!("{}/1", RadarrEvent::GetMovieDetails.resource()); + let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); + let response: Movie = serde_json::from_str(MOVIE_JSON).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -1059,25 +1312,27 @@ mod test { BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::GetMovieDetails) - .await; - - async_server.assert_async().await; - assert!(app_arc - .lock() + if let RadarrSerdeable::Movie(movie) = network + .handle_radarr_event(RadarrEvent::GetMovieDetails(None)) .await - .data - .radarr_data - .movie_details_modal - .is_some()); + .unwrap() + { + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .radarr_data + .movie_details_modal + .is_some()); + assert_eq!(movie, response); - let app = app_arc.lock().await; - let movie_details_modal = app.data.radarr_data.movie_details_modal.as_ref().unwrap(); - assert_str_eq!( - movie_details_modal.movie_details.get_text(), - formatdoc!( - "Title: Test + let app = app_arc.lock().await; + let movie_details_modal = app.data.radarr_data.movie_details_modal.as_ref().unwrap(); + assert_str_eq!( + movie_details_modal.movie_details.get_text(), + formatdoc!( + "Title: Test Year: 2023 Runtime: 2h 0m Rating: R @@ -1092,39 +1347,64 @@ mod test { Path: /nfs/movies Studio: 21st Century Alex Genres: cool, family, fun" - ) - ); - assert_str_eq!( - movie_details_modal.file_details, - formatdoc!( - "Relative Path: Test.mkv + ) + ); + assert_str_eq!( + movie_details_modal.file_details, + formatdoc!( + "Relative Path: Test.mkv Absolute Path: /nfs/movies/Test.mkv Size: 3.30 GB Date Added: 2022-12-30 07:37:56 UTC" - ) - ); - assert_str_eq!( - movie_details_modal.audio_details, - formatdoc!( - "Bitrate: 0 + ) + ); + assert_str_eq!( + movie_details_modal.audio_details, + formatdoc!( + "Bitrate: 0 Channels: 7.1 Codec: AAC Languages: eng Stream Count: 1" - ) - ); - assert_str_eq!( - movie_details_modal.video_details, - formatdoc!( - "Bit Depth: 10 + ) + ); + assert_str_eq!( + movie_details_modal.video_details, + formatdoc!( + "Bit Depth: 10 Bitrate: 0 Codec: x265 FPS: 23.976 Resolution: 1920x804 Scan Type: Progressive Runtime: 2:00:00" - ) - ); + ) + ); + } + } + + #[tokio::test] + async fn test_handle_get_movie_details_event_uses_provided_id() { + let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); + let response: Movie = serde_json::from_str(MOVIE_JSON).unwrap(); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(MOVIE_JSON).unwrap()), + None, + &resource, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + if let RadarrSerdeable::Movie(movie) = network + .handle_radarr_event(RadarrEvent::GetMovieDetails(Some(1))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!(movie, response); + } } #[tokio::test] @@ -1151,7 +1431,7 @@ mod test { "minimumAvailability": "released", "ratings": {} }); - let resource = format!("{}/1", RadarrEvent::GetMovieDetails.resource()); + let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -1171,9 +1451,10 @@ mod test { BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::GetMovieDetails) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::GetMovieDetails(None)) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -1220,7 +1501,12 @@ mod test { "date": "2022-12-30T07:37:56Z", "eventType": "grabbed" }]); - let resource = format!("{}?movieId=1", RadarrEvent::GetMovieHistory.resource()); + let response: Vec = + serde_json::from_value(movie_history_item_json.clone()).unwrap(); + let resource = format!( + "{}?movieId=1", + RadarrEvent::GetMovieHistory(None).resource() + ); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -1239,24 +1525,62 @@ mod test { app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::GetMovieHistory) - .await; + if let RadarrSerdeable::MovieHistoryItems(history) = network + .handle_radarr_event(RadarrEvent::GetMovieHistory(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .radarr_data + .movie_details_modal + .as_ref() + .unwrap() + .movie_history + .items, + vec![movie_history_item()] + ); + assert_eq!(history, response); + } + } - async_server.assert_async().await; - assert_eq!( - app_arc - .lock() - .await - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .items, - vec![movie_history_item()] + #[tokio::test] + async fn test_handle_get_movie_history_event_uses_provided_id() { + let movie_history_item_json = json!([{ + "sourceTitle": "Test", + "quality": { "quality": { "name": "HD - 1080p" }}, + "languages": [ { "name": "English" } ], + "date": "2022-12-30T07:37:56Z", + "eventType": "grabbed" + }]); + let response: Vec = + serde_json::from_value(movie_history_item_json.clone()).unwrap(); + let resource = format!( + "{}?movieId=1", + RadarrEvent::GetMovieHistory(None).resource() ); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(movie_history_item_json), + None, + &resource, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + if let RadarrSerdeable::MovieHistoryItems(history) = network + .handle_radarr_event(RadarrEvent::GetMovieHistory(Some(1))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!(history, response); + } } #[tokio::test] @@ -1268,7 +1592,10 @@ mod test { "date": "2022-12-30T07:37:56Z", "eventType": "grabbed" }]); - let resource = format!("{}?movieId=1", RadarrEvent::GetMovieHistory.resource()); + let resource = format!( + "{}?movieId=1", + RadarrEvent::GetMovieHistory(None).resource() + ); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -1286,9 +1613,10 @@ mod test { .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::GetMovieHistory) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::GetMovieHistory(None)) + .await + .is_ok()); async_server.assert_async().await; assert_eq!( @@ -1382,6 +1710,7 @@ mod test { }, }, }]}); + let response: BlocklistResponse = serde_json::from_value(blocklist_json.clone()).unwrap(); let mut expected_blocklist = vec![ BlocklistItem { id: 123, @@ -1433,14 +1762,19 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetBlocklist).await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.blocklist.items, - expected_blocklist - ); - assert!(app_arc.lock().await.data.radarr_data.blocklist.sort_asc); + if let RadarrSerdeable::BlocklistResponse(blocklist) = network + .handle_radarr_event(RadarrEvent::GetBlocklist) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.blocklist.items, + expected_blocklist + ); + assert!(app_arc.lock().await.data.radarr_data.blocklist.sort_asc); + assert_eq!(blocklist, response); + } } #[tokio::test] @@ -1549,7 +1883,10 @@ mod test { .sorting(vec![blocklist_sort_option]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetBlocklist).await; + assert!(network + .handle_radarr_event(RadarrEvent::GetBlocklist) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -1624,6 +1961,7 @@ mod test { } }], }]); + let response: Vec = serde_json::from_value(collections_json.clone()).unwrap(); let mut expected_collections = vec![ Collection { id: 123, @@ -1668,16 +2006,19 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network + if let RadarrSerdeable::Collections(collections) = network .handle_radarr_event(RadarrEvent::GetCollections) - .await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.collections.items, - expected_collections - ); - assert!(app_arc.lock().await.data.radarr_data.collections.sort_asc); + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.collections.items, + expected_collections + ); + assert!(app_arc.lock().await.data.radarr_data.collections.sort_asc); + assert_eq!(collections, response); + } } #[tokio::test] @@ -1772,9 +2113,10 @@ mod test { .sorting(vec![collection_sort_option]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network + assert!(network .handle_radarr_event(RadarrEvent::GetCollections) - .await; + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -1803,6 +2145,8 @@ mod test { "downloadClient": "transmission", }] }); + let response: DownloadsResponse = + serde_json::from_value(downloads_response_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -1813,13 +2157,18 @@ mod test { .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetDownloads).await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.downloads.items, - downloads_response().records - ); + if let RadarrSerdeable::DownloadsResponse(downloads) = network + .handle_radarr_event(RadarrEvent::GetDownloads) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.downloads.items, + downloads_response().records + ); + assert_eq!(downloads, response); + } } #[tokio::test] @@ -1854,6 +2203,7 @@ mod test { "tags": [1], "id": 1 }]); + let response: Vec = serde_json::from_value(indexers_response_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -1864,17 +2214,22 @@ mod test { .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetIndexers).await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.indexers.items, - vec![indexer()] - ); + if let RadarrSerdeable::Indexers(indexers) = network + .handle_radarr_event(RadarrEvent::GetIndexers) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.indexers.items, + vec![indexer()] + ); + assert_eq!(indexers, response); + } } #[tokio::test] - async fn test_handle_get_indexer_settings_event() { + async fn test_handle_get_all_indexer_settings_event() { let indexer_settings_response_json = json!({ "minimumAge": 0, "maximumSize": 0, @@ -1886,29 +2241,34 @@ mod test { "whitelistedHardcodedSubs": "", "id": 1 }); + let response: IndexerSettings = + serde_json::from_value(indexer_settings_response_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, Some(indexer_settings_response_json), None, - RadarrEvent::GetIndexerSettings.resource(), + RadarrEvent::GetAllIndexerSettings.resource(), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::GetIndexerSettings) - .await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.indexer_settings, - Some(indexer_settings()) - ); + if let RadarrSerdeable::IndexerSettings(settings) = network + .handle_radarr_event(RadarrEvent::GetAllIndexerSettings) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.indexer_settings, + Some(indexer_settings()) + ); + assert_eq!(settings, response); + } } #[tokio::test] - async fn test_handle_get_indexer_settings_event_no_op_if_already_present() { + async fn test_handle_get_all_indexer_settings_event_no_op_if_already_present() { let indexer_settings_response_json = json!({ "minimumAge": 0, "maximumSize": 0, @@ -1925,15 +2285,16 @@ mod test { None, Some(indexer_settings_response_json), None, - RadarrEvent::GetIndexerSettings.resource(), + RadarrEvent::GetAllIndexerSettings.resource(), ) .await; app_arc.lock().await.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::GetIndexerSettings) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::GetAllIndexerSettings) + .await + .is_ok()); async_server.assert_async().await; assert_eq!( @@ -1954,6 +2315,7 @@ mod test { "duration": "00:00:00.5111547", "trigger": "scheduled", }]); + let response: Vec = serde_json::from_value(queued_events_json.clone()).unwrap(); let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); let expected_event = QueueEvent { name: "RefreshMonitoredDownloads".to_owned(), @@ -1976,22 +2338,25 @@ mod test { .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network + if let RadarrSerdeable::QueueEvents(events) = network .handle_radarr_event(RadarrEvent::GetQueuedEvents) - .await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.queued_events.items, - vec![expected_event] - ); + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.queued_events.items, + vec![expected_event] + ); + assert_eq!(events, response); + } } #[tokio::test] async fn test_handle_get_logs_event() { let resource = format!( "{}?pageSize=500&sortDirection=descending&sortKey=time", - RadarrEvent::GetLogs.resource() + RadarrEvent::GetLogs(None).resource() ); let expected_logs = vec![ HorizontallyScrollableText::from( @@ -1999,55 +2364,132 @@ mod test { ), HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), ]; + let logs_response_json = json!({ + "page": 1, + "pageSize": 500, + "sortKey": "time", + "sortDirection": "descending", + "totalRecords": 2, + "records": [ + { + "time": "2023-05-20T21:29:16Z", + "level": "info", + "logger": "TestLogger", + "message": "test message", + "id": 1 + }, + { + "time": "2023-05-20T21:29:16Z", + "level": "fatal", + "logger": "RadarrError", + "exception": "test exception", + "exceptionType": "Some.Big.Bad.Exception", + "id": 2 + } + ] + }); + let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, - Some(json!({ - "page": 1, - "pageSize": 500, - "sortKey": "time", - "sortDirection": "descending", - "totalRecords": 2, - "records": [ - { - "time": "2023-05-20T21:29:16Z", - "level": "info", - "logger": "TestLogger", - "message": "test message", - "id": 1 - }, - { - "time": "2023-05-20T21:29:16Z", - "level": "fatal", - "logger": "RadarrError", - "exception": "test exception", - "exceptionType": "Some.Big.Bad.Exception", - "id": 2 - } - ] - })), + Some(logs_response_json), None, &resource, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetLogs).await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.logs.items, - expected_logs - ); - assert!(app_arc - .lock() + if let RadarrSerdeable::LogResponse(logs) = network + .handle_radarr_event(RadarrEvent::GetLogs(None)) .await - .data - .radarr_data - .logs - .current_selection() - .text - .contains("INFO")); + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.logs.items, + expected_logs + ); + assert!(app_arc + .lock() + .await + .data + .radarr_data + .logs + .current_selection() + .text + .contains("INFO")); + assert_eq!(logs, response); + } + } + + #[tokio::test] + async fn test_handle_get_logs_event_uses_provided_events() { + let resource = format!( + "{}?pageSize=1000&sortDirection=descending&sortKey=time", + RadarrEvent::GetLogs(Some(1000)).resource() + ); + let expected_logs = vec![ + HorizontallyScrollableText::from( + "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", + ), + HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), + ]; + let logs_response_json = json!({ + "page": 1, + "pageSize": 1000, + "sortKey": "time", + "sortDirection": "descending", + "totalRecords": 2, + "records": [ + { + "time": "2023-05-20T21:29:16Z", + "level": "info", + "logger": "TestLogger", + "message": "test message", + "id": 1 + }, + { + "time": "2023-05-20T21:29:16Z", + "level": "fatal", + "logger": "RadarrError", + "exception": "test exception", + "exceptionType": "Some.Big.Bad.Exception", + "id": 2 + } + ] + }); + let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(logs_response_json), + None, + &resource, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + if let RadarrSerdeable::LogResponse(logs) = network + .handle_radarr_event(RadarrEvent::GetLogs(Some(1000))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.logs.items, + expected_logs + ); + assert!(app_arc + .lock() + .await + .data + .radarr_data + .logs + .current_selection() + .text + .contains("INFO")); + assert_eq!(logs, response); + } } #[tokio::test] @@ -2056,6 +2498,8 @@ mod test { "id": 2222, "name": "HD - 1080p" }]); + let response: Vec = + serde_json::from_value(quality_profile_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -2066,15 +2510,18 @@ mod test { .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network + if let RadarrSerdeable::QualityProfiles(quality_profiles) = network .handle_radarr_event(RadarrEvent::GetQualityProfiles) - .await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.quality_profile_map, - BiMap::from_iter([(2222i64, "HD - 1080p".to_owned())]) - ); + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.quality_profile_map, + BiMap::from_iter([(2222i64, "HD - 1080p".to_owned())]) + ); + assert_eq!(quality_profiles, response); + } } #[tokio::test] @@ -2083,6 +2530,7 @@ mod test { "id": 2222, "label": "usenet" }]); + let response: Vec = serde_json::from_value(tags_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -2093,13 +2541,18 @@ mod test { .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetTags).await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.tags_map, - BiMap::from_iter([(2222i64, "usenet".to_owned())]) - ); + if let RadarrSerdeable::Tags(tags) = network + .handle_radarr_event(RadarrEvent::GetTags) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.tags_map, + BiMap::from_iter([(2222i64, "usenet".to_owned())]) + ); + assert_eq!(tags, response); + } } #[tokio::test] @@ -2120,11 +2573,12 @@ mod test { "nextExecution": "2023-05-20T21:29:16Z", "lastDuration": "00:00:00.5111547", }]); + let response: Vec = serde_json::from_value(tasks_json.clone()).unwrap(); let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); let expected_tasks = vec![ Task { name: "Application Check Update".to_owned(), - task_name: "ApplicationCheckUpdate".to_owned(), + task_name: TaskName::ApplicationCheckUpdate, interval: 360, last_execution: timestamp, next_execution: timestamp, @@ -2132,7 +2586,7 @@ mod test { }, Task { name: "Backup".to_owned(), - task_name: "Backup".to_owned(), + task_name: TaskName::Backup, interval: 10080, last_execution: timestamp, next_execution: timestamp, @@ -2149,18 +2603,23 @@ mod test { .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetTasks).await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.tasks.items, - expected_tasks - ); + if let RadarrSerdeable::Tasks(tasks) = network + .handle_radarr_event(RadarrEvent::GetTasks) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.tasks.items, + expected_tasks + ); + assert_eq!(tasks, response); + } } #[tokio::test] async fn test_handle_get_updates_event() { - let tasks_json = json!([{ + let updates_json = json!([{ "version": "4.3.2.1", "releaseDate": "2023-04-15T02:02:53Z", "installed": true, @@ -2200,6 +2659,7 @@ mod test { ] }, }]); + let response: Vec = serde_json::from_value(updates_json.clone()).unwrap(); let line_break = "-".repeat(200); let expected_text = ScrollableText::with_string(formatdoc!( " @@ -2229,28 +2689,35 @@ mod test { let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, - Some(tasks_json), + Some(updates_json), None, RadarrEvent::GetUpdates.resource(), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::GetUpdates).await; - - async_server.assert_async().await; - assert_str_eq!( - app_arc.lock().await.data.radarr_data.updates.get_text(), - expected_text.get_text() - ); + if let RadarrSerdeable::Updates(updates) = network + .handle_radarr_event(RadarrEvent::GetUpdates) + .await + .unwrap() + { + async_server.assert_async().await; + assert_str_eq!( + app_arc.lock().await.data.radarr_data.updates.get_text(), + expected_text.get_text() + ); + assert_eq!(updates, response); + } } #[tokio::test] async fn test_add_tag() { + let tag_json = json!({ "id": 3, "label": "testing" }); + let response: Tag = serde_json::from_value(tag_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Post, Some(json!({ "label": "testing" })), - Some(json!({ "id": 3, "label": "testing" })), + Some(tag_json), None, RadarrEvent::GetTags.resource(), ) @@ -2259,17 +2726,37 @@ mod test { BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.add_tag("testing".to_owned()).await; + if let RadarrSerdeable::Tag(tag) = network + .handle_radarr_event(RadarrEvent::AddTag("testing".to_owned())) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.tags_map, + BiMap::from_iter([ + (1, "usenet".to_owned()), + (2, "test".to_owned()), + (3, "testing".to_owned()) + ]) + ); + assert_eq!(tag, response); + } + } + + #[tokio::test] + async fn test_handle_delete_tag_event() { + let resource = format!("{}/1", RadarrEvent::DeleteTag(1).resource()); + let (async_server, app_arc, _server) = + mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::DeleteTag(1)) + .await + .is_ok()); async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.tags_map, - BiMap::from_iter([ - (1, "usenet".to_owned()), - (2, "test".to_owned()), - (3, "testing".to_owned()) - ]) - ); } #[tokio::test] @@ -2280,6 +2767,7 @@ mod test { "accessible": true, "freeSpace": 219902325555200u64, }]); + let response: Vec = serde_json::from_value(root_folder_json.clone()).unwrap(); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -2290,15 +2778,18 @@ mod test { .await; let mut network = Network::new(&app_arc, CancellationToken::new()); - network + if let RadarrSerdeable::RootFolders(root_folders) = network .handle_radarr_event(RadarrEvent::GetRootFolders) - .await; - - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.root_folders.items, - vec![root_folder()] - ); + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.root_folders.items, + vec![root_folder()] + ); + assert_eq!(root_folders, response); + } } #[tokio::test] @@ -2316,7 +2807,11 @@ mod test { "type": "crew", } ]); - let resource = format!("{}?movieId=1", RadarrEvent::GetMovieCredits.resource()); + let response: Vec = serde_json::from_value(credits_json.clone()).unwrap(); + let resource = format!( + "{}?movieId=1", + RadarrEvent::GetMovieCredits(None).resource() + ); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -2335,16 +2830,59 @@ mod test { app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::GetMovieCredits) - .await; + if let RadarrSerdeable::Credits(credits) = network + .handle_radarr_event(RadarrEvent::GetMovieCredits(None)) + .await + .unwrap() + { + let app = app_arc.lock().await; + let movie_details_modal = app.data.radarr_data.movie_details_modal.as_ref().unwrap(); - let app = app_arc.lock().await; - let movie_details_modal = app.data.radarr_data.movie_details_modal.as_ref().unwrap(); + async_server.assert_async().await; + assert_eq!(movie_details_modal.movie_cast.items, vec![cast_credit()]); + assert_eq!(movie_details_modal.movie_crew.items, vec![crew_credit()]); + assert_eq!(credits, response); + } + } - async_server.assert_async().await; - assert_eq!(movie_details_modal.movie_cast.items, vec![cast_credit()]); - assert_eq!(movie_details_modal.movie_crew.items, vec![crew_credit()]); + #[tokio::test] + async fn test_handle_get_movie_credits_event_uses_provided_id() { + let credits_json = json!([ + { + "personName": "Madison Clarke", + "character": "Johnny Blaze", + "type": "cast", + }, + { + "personName": "Alex Clarke", + "department": "Music", + "job": "Composition", + "type": "crew", + } + ]); + let response: Vec = serde_json::from_value(credits_json.clone()).unwrap(); + let resource = format!( + "{}?movieId=1", + RadarrEvent::GetMovieCredits(None).resource() + ); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(credits_json), + None, + &resource, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + if let RadarrSerdeable::Credits(credits) = network + .handle_radarr_event(RadarrEvent::GetMovieCredits(Some(1))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!(credits, response); + } } #[tokio::test] @@ -2362,7 +2900,10 @@ mod test { "type": "crew", } ]); - let resource = format!("{}?movieId=1", RadarrEvent::GetMovieCredits.resource()); + let resource = format!( + "{}?movieId=1", + RadarrEvent::GetMovieCredits(None).resource() + ); let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, @@ -2380,9 +2921,10 @@ mod test { .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::GetMovieCredits) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::GetMovieCredits(None)) + .await + .is_ok()); let app = app_arc.lock().await; let movie_details_modal = app.data.radarr_data.movie_details_modal.as_ref().unwrap(); @@ -2396,7 +2938,7 @@ mod test { async fn test_handle_delete_movie_event() { let resource = format!( "{}/1?deleteFiles=true&addImportExclusion=true", - RadarrEvent::DeleteMovie.resource() + RadarrEvent::DeleteMovie(None).resource() ); let (async_server, app_arc, _server) = mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; @@ -2408,7 +2950,35 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::DeleteMovie).await; + assert!(network + .handle_radarr_event(RadarrEvent::DeleteMovie(None)) + .await + .is_ok()); + + async_server.assert_async().await; + assert!(!app_arc.lock().await.data.radarr_data.delete_movie_files); + assert!(!app_arc.lock().await.data.radarr_data.add_list_exclusion); + } + + #[tokio::test] + async fn test_handle_delete_movie_event_use_provided_params() { + let resource = format!( + "{}/1?deleteFiles=true&addImportExclusion=true", + RadarrEvent::DeleteMovie(None).resource() + ); + let (async_server, app_arc, _server) = + mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + let delete_movie_params = DeleteMovieParams { + id: 1, + delete_movie_files: true, + add_list_exclusion: true, + }; + + assert!(network + .handle_radarr_event(RadarrEvent::DeleteMovie(Some(delete_movie_params))) + .await + .is_ok()); async_server.assert_async().await; assert!(!app_arc.lock().await.data.radarr_data.delete_movie_files); @@ -2449,16 +3019,17 @@ mod test { .set_items(blocklist_items); let mut network = Network::new(&app_arc, CancellationToken::new()); - network + assert!(network .handle_radarr_event(RadarrEvent::ClearBlocklist) - .await; + .await + .is_ok()); async_server.assert_async().await; } #[tokio::test] async fn test_handle_delete_blocklist_item_event() { - let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem.resource()); + let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem(None).resource()); let (async_server, app_arc, _server) = mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; app_arc @@ -2470,16 +3041,32 @@ mod test { .set_items(vec![blocklist_item()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::DeleteBlocklistItem) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::DeleteBlocklistItem(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_delete_blocklist_item_event_uses_provided_id() { + let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem(None).resource()); + let (async_server, app_arc, _server) = + mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::DeleteBlocklistItem(Some(1))) + .await + .is_ok()); async_server.assert_async().await; } #[tokio::test] async fn test_handle_delete_download_event() { - let resource = format!("{}/1", RadarrEvent::DeleteDownload.resource()); + let resource = format!("{}/1", RadarrEvent::DeleteDownload(None).resource()); let (async_server, app_arc, _server) = mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; app_arc @@ -2491,16 +3078,32 @@ mod test { .set_items(vec![download_record()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::DeleteDownload) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::DeleteDownload(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_delete_download_event_uses_provided_id() { + let resource = format!("{}/1", RadarrEvent::DeleteDownload(None).resource()); + let (async_server, app_arc, _server) = + mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::DeleteDownload(Some(1))) + .await + .is_ok()); async_server.assert_async().await; } #[tokio::test] async fn test_handle_delete_indexer_event() { - let resource = format!("{}/1", RadarrEvent::DeleteIndexer.resource()); + let resource = format!("{}/1", RadarrEvent::DeleteIndexer(None).resource()); let (async_server, app_arc, _server) = mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; app_arc @@ -2512,16 +3115,32 @@ mod test { .set_items(vec![indexer()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::DeleteIndexer) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::DeleteIndexer(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_delete_indexer_event_uses_provided_id() { + let resource = format!("{}/1", RadarrEvent::DeleteIndexer(None).resource()); + let (async_server, app_arc, _server) = + mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::DeleteIndexer(Some(1))) + .await + .is_ok()); async_server.assert_async().await; } #[tokio::test] async fn test_handle_delete_root_folder_event() { - let resource = format!("{}/1", RadarrEvent::DeleteRootFolder.resource()); + let resource = format!("{}/1", RadarrEvent::DeleteRootFolder(None).resource()); let (async_server, app_arc, _server) = mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; app_arc @@ -2533,9 +3152,25 @@ mod test { .set_items(vec![root_folder()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::DeleteRootFolder) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::DeleteRootFolder(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_delete_root_folder_event_uses_provided_id() { + let resource = format!("{}/1", RadarrEvent::DeleteRootFolder(None).resource()); + let (async_server, app_arc, _server) = + mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::DeleteRootFolder(Some(1))) + .await + .is_ok()); async_server.assert_async().await; } @@ -2558,9 +3193,9 @@ mod test { "searchForMovie": true } })), + Some(json!({})), None, - None, - RadarrEvent::AddMovie.resource(), + RadarrEvent::AddMovie(None).resource(), ) .await; @@ -2616,7 +3251,63 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::AddMovie).await; + assert!(network + .handle_radarr_event(RadarrEvent::AddMovie(None)) + .await + .is_ok()); + + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .radarr_data + .add_movie_modal + .is_none()); + } + + #[tokio::test] + async fn test_handle_add_movie_event_uses_provided_body() { + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Post, + Some(json!({ + "tmdbId": 1234, + "title": "Test", + "rootFolderPath": "/nfs2", + "minimumAvailability": "announced", + "monitored": true, + "qualityProfileId": 2222, + "tags": [1, 2], + "addOptions": { + "monitor": "movieOnly", + "searchForMovie": true + } + })), + Some(json!({})), + None, + RadarrEvent::AddMovie(None).resource(), + ) + .await; + let body = AddMovieBody { + tmdb_id: 1234, + title: "Test".to_owned(), + root_folder_path: "/nfs2".to_owned(), + minimum_availability: "announced".to_owned(), + monitored: true, + quality_profile_id: 2222, + tags: vec![1, 2], + add_options: AddOptions { + monitor: "movieOnly".to_owned(), + search_for_movie: true, + }, + }; + + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::AddMovie(Some(body))) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -2645,9 +3336,9 @@ mod test { "searchForMovie": true } })), + Some(json!({})), None, - None, - RadarrEvent::AddMovie.resource(), + RadarrEvent::AddMovie(None).resource(), ) .await; @@ -2699,7 +3390,10 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::AddMovie).await; + assert!(network + .handle_radarr_event(RadarrEvent::AddMovie(None)) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -2731,18 +3425,49 @@ mod test { Some(json!({ "path": "/nfs/test" })), + Some(json!({})), None, - None, - RadarrEvent::AddRootFolder.resource(), + RadarrEvent::AddRootFolder(None).resource(), ) .await; app_arc.lock().await.data.radarr_data.edit_root_folder = Some("/nfs/test".into()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::AddRootFolder) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::AddRootFolder(None)) + .await + .is_ok()); + + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .radarr_data + .edit_root_folder + .is_none()); + } + + #[tokio::test] + async fn test_handle_add_root_folder_event_uses_provided_path() { + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Post, + Some(json!({ + "path": "/test/test" + })), + Some(json!({})), + None, + RadarrEvent::AddRootFolder(None).resource(), + ) + .await; + + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::AddRootFolder(Some("/test/test".to_owned()))) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -2772,16 +3497,17 @@ mod test { Some(indexer_settings_json), None, None, - RadarrEvent::EditAllIndexerSettings.resource(), + RadarrEvent::EditAllIndexerSettings(None).resource(), ) .await; app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::EditAllIndexerSettings) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::EditAllIndexerSettings(None)) + .await + .is_ok()); async_server.assert_async().await; assert!(app_arc @@ -2793,6 +3519,40 @@ mod test { .is_none()); } + #[tokio::test] + async fn test_handle_edit_all_indexer_settings_event_uses_provided_settings() { + let indexer_settings_json = json!({ + "minimumAge": 0, + "maximumSize": 0, + "retention": 0, + "rssSyncInterval": 60, + "preferIndexerFlags": false, + "availabilityDelay": 0, + "allowHardcodedSubs": true, + "whitelistedHardcodedSubs": "", + "id": 1 + }); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Put, + Some(indexer_settings_json), + None, + None, + RadarrEvent::EditAllIndexerSettings(None).resource(), + ) + .await; + + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditAllIndexerSettings( + Some(indexer_settings()) + )) + .await + .is_ok()); + + async_server.assert_async().await; + } + #[tokio::test] async fn test_handle_edit_collection_event() { let detailed_collection_body = json!({ @@ -2845,7 +3605,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/123", RadarrEvent::EditCollection.resource()).as_str(), + format!( + "/api/v3{}/123", + RadarrEvent::EditCollection(None).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -2877,9 +3641,10 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::EditCollection) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::EditCollection(None)) + .await + .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; @@ -2888,6 +3653,167 @@ mod test { assert!(app.data.radarr_data.edit_collection_modal.is_none()); } + #[tokio::test] + async fn test_handle_edit_collection_event_uses_provided_parameters() { + let detailed_collection_body = json!({ + "id": 123, + "title": "Test Collection", + "rootFolderPath": "/nfs/movies", + "searchOnAdd": true, + "monitored": true, + "minimumAvailability": "released", + "overview": "Collection blah blah blah", + "qualityProfileId": 2222, + "movies": [ + { + "title": "Test", + "overview": "Collection blah blah blah", + "year": 2023, + "runtime": 120, + "tmdbId": 1234, + "genres": ["cool", "family", "fun"], + "ratings": { + "imdb": { + "value": 9.9 + }, + "tmdb": { + "value": 9.9 + }, + "rottenTomatoes": { + "value": 9.9 + } + } + } + ] + }); + let mut expected_body = detailed_collection_body.clone(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + *expected_body.get_mut("minimumAvailability").unwrap() = json!("announced"); + *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); + *expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/Test Path"); + *expected_body.get_mut("searchOnAdd").unwrap() = json!(false); + + let resource = format!("{}/123", RadarrEvent::GetCollections.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(detailed_collection_body), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v3{}/123", + RadarrEvent::EditCollection(None).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + let edit_collection_params = EditCollectionParams { + collection_id: 123, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + search_on_add: Some(false), + }; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditCollection(Some(edit_collection_params))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_collection_event_uses_provided_parameters_defaults_to_previous_values_when_none( + ) { + let detailed_collection_body = json!({ + "id": 123, + "title": "Test Collection", + "rootFolderPath": "/nfs/movies", + "searchOnAdd": true, + "monitored": true, + "minimumAvailability": "released", + "overview": "Collection blah blah blah", + "qualityProfileId": 2222, + "movies": [ + { + "title": "Test", + "overview": "Collection blah blah blah", + "year": 2023, + "runtime": 120, + "tmdbId": 1234, + "genres": ["cool", "family", "fun"], + "ratings": { + "imdb": { + "value": 9.9 + }, + "tmdb": { + "value": 9.9 + }, + "rottenTomatoes": { + "value": 9.9 + } + } + } + ] + }); + let mut expected_body = detailed_collection_body.clone(); + *expected_body.get_mut("monitored").unwrap() = json!(true); + *expected_body.get_mut("minimumAvailability").unwrap() = json!("released"); + *expected_body.get_mut("qualityProfileId").unwrap() = json!(2222); + *expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/movies"); + *expected_body.get_mut("searchOnAdd").unwrap() = json!(true); + + let resource = format!("{}/123", RadarrEvent::GetCollections.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(detailed_collection_body), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v3{}/123", + RadarrEvent::EditCollection(None).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + let edit_collection_params = EditCollectionParams { + collection_id: 123, + ..EditCollectionParams::default() + }; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditCollection(Some(edit_collection_params))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + #[tokio::test] async fn test_handle_edit_indexer_event() { let indexer_details_json = json!({ @@ -2895,6 +3821,7 @@ mod test { "enableAutomaticSearch": true, "enableInteractiveSearch": true, "name": "Test Indexer", + "priority": 1, "fields": [ { "name": "baseUrl", @@ -2917,6 +3844,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", + "priority": 1, "fields": [ { "name": "baseUrl", @@ -2947,7 +3875,7 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(), + format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -2973,7 +3901,10 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::EditIndexer).await; + assert!(network + .handle_radarr_event(RadarrEvent::EditIndexer(None)) + .await + .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; @@ -2990,6 +3921,7 @@ mod test { "enableAutomaticSearch": true, "enableInteractiveSearch": true, "name": "Test Indexer", + "priority": 1, "fields": [ { "name": "baseUrl", @@ -3008,6 +3940,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", + "priority": 1, "fields": [ { "name": "baseUrl", @@ -3034,7 +3967,7 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(), + format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3069,7 +4002,10 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::EditIndexer).await; + assert!(network + .handle_radarr_event(RadarrEvent::EditIndexer(None)) + .await + .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; @@ -3086,6 +4022,7 @@ mod test { "enableAutomaticSearch": true, "enableInteractiveSearch": true, "name": "Test Indexer", + "priority": 1, "fields": [ { "name": "baseUrl", @@ -3107,6 +4044,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", + "priority": 1, "fields": [ { "name": "baseUrl", @@ -3137,7 +4075,7 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(), + format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3179,7 +4117,10 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::EditIndexer).await; + assert!(network + .handle_radarr_event(RadarrEvent::EditIndexer(None)) + .await + .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; @@ -3188,6 +4129,243 @@ mod test { assert!(app.data.radarr_data.edit_indexer_modal.is_none()); } + #[tokio::test] + async fn test_handle_edit_indexer_event_uses_provided_parameters() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 25, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tags: Some(vec![1, 2]), + priority: Some(25), + ..EditIndexerParams::default() + }; + + let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_indexer_event_uses_provided_parameters_defaults_to_previous_values() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + ..EditIndexerParams::default() + }; + + let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json.clone()), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(indexer_details_json)) + .create_async() + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_indexer_event_uses_provided_parameters_clears_tags_when_clear_tags_is_true( + ) { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let expected_edit_indexer_body = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [], + "id": 1 + }); + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + clear_tags: true, + ..EditIndexerParams::default() + }; + + let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_edit_indexer_body)) + .create_async() + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + #[tokio::test] async fn test_handle_edit_movie_event() { let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); @@ -3197,7 +4375,7 @@ mod test { *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("tags").unwrap() = json!([1, 2]); - let resource = format!("{}/1", RadarrEvent::GetMovieDetails.resource()); + let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); let (async_details_server, app_arc, mut server) = mock_radarr_api( RequestMethod::Get, None, @@ -3209,7 +4387,7 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie.resource()).as_str(), + format!("/api/v3{}/1", RadarrEvent::EditMovie(None).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3242,7 +4420,10 @@ mod test { } let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::EditMovie).await; + assert!(network + .handle_radarr_event(RadarrEvent::EditMovie(None)) + .await + .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; @@ -3251,6 +4432,132 @@ mod test { assert!(app.data.radarr_data.edit_movie_modal.is_none()); } + #[tokio::test] + async fn test_handle_edit_movie_event_uses_provided_parameters() { + let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + *expected_body.get_mut("minimumAvailability").unwrap() = json!("announced"); + *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); + *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); + *expected_body.get_mut("tags").unwrap() = json!([1, 2]); + + let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(MOVIE_JSON).unwrap()), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditMovie(None).resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + let edit_movie_params = EditMovieParams { + movie_id: 1, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tags: Some(vec![1, 2]), + ..EditMovieParams::default() + }; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_movie_event_uses_provided_parameters_defaults_to_previous_values() { + let expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); + let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(MOVIE_JSON).unwrap()), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditMovie(None).resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + let edit_movie_params = EditMovieParams { + movie_id: 1, + ..EditMovieParams::default() + }; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_movie_event_uses_provided_parameters_returns_empty_tags_vec_when_clear_tags_is_true( + ) { + let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); + *expected_body.get_mut("tags").unwrap() = json!([]); + + let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(MOVIE_JSON).unwrap()), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditMovie(None).resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + let edit_movie_params = EditMovieParams { + movie_id: 1, + clear_tags: true, + ..EditMovieParams::default() + }; + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + #[tokio::test] async fn test_handle_download_release_event() { let (async_server, app_arc, _server) = mock_radarr_api( @@ -3260,9 +4567,9 @@ mod test { "indexerId": 2, "movieId": 1 })), + Some(json!({})), None, - None, - RadarrEvent::DownloadRelease.resource(), + RadarrEvent::DownloadRelease(None).resource(), ) .await; let mut movie_details_modal = MovieDetailsModal::default(); @@ -3279,9 +4586,39 @@ mod test { .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new()); - network - .handle_radarr_event(RadarrEvent::DownloadRelease) - .await; + assert!(network + .handle_radarr_event(RadarrEvent::DownloadRelease(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_download_release_event_uses_provided_params() { + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Post, + Some(json!({ + "guid": "1234", + "indexerId": 2, + "movieId": 1 + })), + Some(json!({})), + None, + RadarrEvent::DownloadRelease(None).resource(), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new()); + let params = ReleaseDownloadBody { + guid: "1234".to_owned(), + indexer_id: 2, + movie_id: 1, + }; + + assert!(network + .handle_radarr_event(RadarrEvent::DownloadRelease(Some(params))) + .await + .is_ok()); async_server.assert_async().await; } @@ -3353,12 +4690,11 @@ mod test { .movies .set_items(vec![Movie { id: 1, - tmdb_id: 2, ..Movie::default() }]); let mut network = Network::new(&app_arc, CancellationToken::new()); - assert_eq!(network.extract_movie_id().await, (1, 2)); + assert_eq!(network.extract_movie_id().await, 1); } #[tokio::test] @@ -3367,13 +4703,12 @@ mod test { let mut filtered_movies = StatefulTable::default(); filtered_movies.set_filtered_items(vec![Movie { id: 1, - tmdb_id: 2, ..Movie::default() }]); app_arc.lock().await.data.radarr_data.movies = filtered_movies; let mut network = Network::new(&app_arc, CancellationToken::new()); - assert_eq!(network.extract_movie_id().await, (1, 2)); + assert_eq!(network.extract_movie_id().await, 1); } #[tokio::test] @@ -3424,11 +4759,32 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new()); assert_str_eq!( - network.append_movie_id_param("/test").await, + network.append_movie_id_param("/test", None).await, "/test?movieId=1" ); } + #[tokio::test] + async fn test_append_movie_id_param_uses_provided_id() { + let app_arc = Arc::new(Mutex::new(App::default())); + app_arc + .lock() + .await + .data + .radarr_data + .movies + .set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new()); + + assert_str_eq!( + network.append_movie_id_param("/test", Some(11)).await, + "/test?movieId=11" + ); + } + #[tokio::test] async fn test_radarr_request_props_from_default_radarr_config() { let app_arc = Arc::new(Mutex::new(App::default())); @@ -3530,11 +4886,7 @@ mod test { response_status: Option, resource: &str, ) -> (Mock, Arc>>, ServerGuard) { - let status = if let Some(status) = response_status { - status - } else { - 200 - }; + let status = response_status.unwrap_or(200); let mut server = Server::new_async().await; let mut async_server = server .mock( diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f725af4..2959710 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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); diff --git a/src/ui/radarr_ui/collections/edit_collection_ui.rs b/src/ui/radarr_ui/collections/edit_collection_ui.rs index 9eedf8d..01a0083 100644 --- a/src/ui/radarr_ui/collections/edit_collection_ui.rs +++ b/src/ui/radarr_ui/collections/edit_collection_ui.rs @@ -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); diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs index d16699d..33ee7c5 100644 --- a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs @@ -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); diff --git a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs index 6e55d7c..631d2ad 100644 --- a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs @@ -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, diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index e087d16..ac80cb2 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -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); diff --git a/src/ui/radarr_ui/library/edit_movie_ui.rs b/src/ui/radarr_ui/library/edit_movie_ui.rs index a1fe967..072950f 100644 --- a/src/ui/radarr_ui/library/edit_movie_ui.rs +++ b/src/ui/radarr_ui/library/edit_movie_ui.rs @@ -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);