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

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