Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb856e28d7 | |||
| 866b0c7537 | |||
| 3ef5c1911d | |||
| 80787d1187 | |||
| ad0b3989ed | |||
| c7a0e33485 | |||
| ee312a21eb | |||
| 3af22cceac | |||
| c29e2ca9ae | |||
| 06c9baf8df | |||
| 97dc5054e9 | |||
| d43862a3a7 | |||
| 1dd4cd74c3 | |||
| 4f86cce497 | |||
| 3968983002 | |||
| 4c7e8f0cf6 | |||
| 1e3141e4ee | |||
| 45542cd3a9 | |||
| da3bb795b7 | |||
| 53a59cdb4c | |||
| 8125bd5ae0 | |||
| 5ba3f2b1ba | |||
| c98828aec7 | |||
| 5ed278ec9c | |||
| c8a2fea9cd | |||
| cac54c5447 | |||
| 374819b4f3 | |||
| 4d92c350de | |||
| 3be9321df6 | |||
| 746064c430 | |||
| ffc00691cb | |||
| 1b5979c36c | |||
| 5ed3372ae2 | |||
| 8002a5aa1e | |||
| 896c50909a | |||
| cea4632a22 | |||
| 7fdec15ba9 | |||
| eb06787bb2 | |||
| c3577a0724 | |||
| 8864e2c867 | |||
| 581975b941 | |||
| b8e4deb80f | |||
| 40bb22ef7c | |||
| 74e9ea17ac | |||
| a11bce603d | |||
| c754275af3 | |||
| 3497a54c39 | |||
| 8807adea83 | |||
| 6896fcc134 | |||
| 68830a8789 | |||
| 2dce587ea8 | |||
| 9403bdcbcb | |||
| aa13735533 | |||
| 33db3efacf | |||
| 8df74585bc | |||
| 16ca8841a1 | |||
| 22fbe025d9 | |||
| c54bd2bab0 | |||
| 539ad75fe6 | |||
| 9476caa392 | |||
| df3cf70682 | |||
| a881d1f33a | |||
| d96316577a | |||
| eefe6392df | |||
| 1cc95e2cd1 | |||
| c5328917de | |||
| 208acafc73 | |||
| 57eced64c0 | |||
| ce701c1ab7 | |||
| b24e3bf9db | |||
| 1227796e78 | |||
| bb1c08277e | |||
| 16538a3158 | |||
| f4c647342b | |||
| 72cb334b6a | |||
| 1a65a7f3e7 | |||
| 6a0049eb8f | |||
| d2e3750de6 | |||
| 4cdad182ef | |||
| 4ed1e99a15 | |||
| 71870d9396 | |||
| fa4ec709c0 | |||
| d7d223400e | |||
| 34157ef32f | |||
| f5631376af | |||
| df1eea22ab | |||
| 86d93377ac | |||
| 5872a6ba72 | |||
| 6da1ae93ef | |||
| b8c60bf59a | |||
| bd2d2875a5 | |||
| 9d782af020 | |||
| a711c3d16c | |||
| 268cc13d27 | |||
| a8328d3636 | |||
| d82a7f7674 | |||
| 5e63c34a9f | |||
| 540db5993b | |||
| 16bf06426f | |||
| cc02832512 | |||
| 2876913f48 | |||
| 6b64b5ecc4 | |||
| 9ceb55a314 | |||
| 7870bb4b5b | |||
| 4fc2d3c94b | |||
| a012945df2 | |||
| f094cf5ad3 | |||
| d8979221c8 | |||
| aaa4e67f43 | |||
| fff38704ab | |||
| d5e6d64d0f | |||
| 003f319385 | |||
| e14b7072c6 | |||
| 1fe95d057b | |||
| 6dffc90e92 | |||
| 295cd56a1f | |||
| 214c89e8b5 | |||
| 29047c3007 | |||
| a8f3bed402 | |||
| 1ca9265a2a | |||
| 60d61b9e31 | |||
| b6f5b9d08c | |||
| 28f7bc6a4c | |||
| 5b42129f55 | |||
| 4f06b2b947 | |||
| 0f98050a12 | |||
| 14839642dc | |||
| eccc1a2df1 | |||
| 9df929a8e3 | |||
| 1e008f9778 | |||
| fa811da5c2 | |||
| 48ad17c6f1 | |||
| 3cd15f34cd | |||
| 53ca14e64d | |||
| 0d8803d35d | |||
| 8c90221a81 | |||
| a708f71d57 | |||
| 2a13f74a2b | |||
| 2a97c49a8e | |||
| 8c155ce656 | |||
| 5245ba6d98 | |||
| f9789ecc9b | |||
| 9936ce1ab5 | |||
| 650c9783a6 | |||
| b253a389eb | |||
| 5023fbd3d1 | |||
| fdb08fbd34 | |||
| b125d3341a | |||
| f73e3a4817 |
@@ -0,0 +1,10 @@
|
|||||||
|
[tool.commitizen]
|
||||||
|
name = "cz_conventional_commits"
|
||||||
|
tag_format = "v$version"
|
||||||
|
version_scheme = "semver"
|
||||||
|
version_provider = "cargo"
|
||||||
|
update_changelog_on_bump = true
|
||||||
|
major_version_zero = true
|
||||||
|
|
||||||
|
[tool.commitizen.hooks]
|
||||||
|
pre-commit = "git add Cargo.toml Cargo.lock"
|
||||||
@@ -1,24 +1,65 @@
|
|||||||
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
|
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
|
||||||
# Thanks to joshka for permission to use this template!
|
# Thanks to joshka for permission to use this template!
|
||||||
|
|
||||||
name: Create Release PR and Publish Release
|
name: Create release
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches:
|
inputs:
|
||||||
- main
|
bump_type:
|
||||||
|
description: "Specify the type of version bump"
|
||||||
|
required: true
|
||||||
|
default: "patch"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- patch
|
||||||
|
- minor
|
||||||
|
- major
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
bump:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x" # Specify your required Python version
|
||||||
|
|
||||||
|
- name: Install Commitizen
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install commitizen
|
||||||
|
|
||||||
|
- name: Bump version with Commitizen
|
||||||
|
run: |
|
||||||
|
cz bump --yes --increment ${{ github.event.inputs.bump_type }}
|
||||||
|
|
||||||
|
- name: Push changes
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
git push origin main --follow-tags
|
||||||
|
|
||||||
release-plz:
|
release-plz:
|
||||||
# see https://release-plz.ieni.dev/docs/github
|
# see https://release-plz.ieni.dev/docs/github
|
||||||
# for more information
|
# for more information
|
||||||
name: Release-plz
|
name: Release-plz
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check if actor is repository owner
|
||||||
|
if: ${{ github.actor != github.repository_owner }}
|
||||||
|
run: |
|
||||||
|
echo "You are not authorized to run this workflow."
|
||||||
|
exit 1
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -27,6 +68,8 @@ jobs:
|
|||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Run release-plz
|
- name: Run release-plz
|
||||||
uses: MarcoIeni/release-plz-action@v0.5
|
uses: MarcoIeni/release-plz-action@v0.5
|
||||||
|
with:
|
||||||
|
command: release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
repos:
|
||||||
|
- hooks:
|
||||||
|
- id: commitizen
|
||||||
|
- id: commitizen-branch
|
||||||
|
stages:
|
||||||
|
- pre-push
|
||||||
|
repo: https://github.com/commitizen-tools/commitizen
|
||||||
|
rev: v3.30.0
|
||||||
+23
-1
@@ -5,7 +5,29 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## v0.2.2 (2024-11-06)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- **handler**: Fixed a bug in the movie details handler that would allow key events to be processed before the data was finished loading
|
||||||
|
- **ui**: Fixed a bug that would freeze all user input while background network requests were running
|
||||||
|
- **radarr_ui**: Fixed a race condition bug in the movie details UI that would panic if the user changes tabs too quickly
|
||||||
|
|
||||||
|
### Perf
|
||||||
|
|
||||||
|
- **network**: Improved performance and reactiveness of the UI by speeding up network requests and clearing the channel whenever a request is cancelled/the UI is routing
|
||||||
|
|
||||||
|
## v0.2.1 (2024-11-06)
|
||||||
|
|
||||||
|
## [0.2.1](https://github.com/Dark-Alex-17/managarr/compare/v0.2.0...v0.2.1) - 2024-11-06
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- Removed the need for use_ssl to indicate SSL usage; instead just use the ssl_cert_path
|
||||||
|
- Applied bug fix to the downloads tab as well as the context [skip ci]
|
||||||
|
- Updated the README to not include the GitHub downloads badge since all binary releases are on crates.io [skip ci]
|
||||||
|
- Set all releases as manually triggered instead of automatic [skip ci]
|
||||||
|
- Updated dockerfile to no longer use the --disable-terminal-size-checks flag [skip ci]
|
||||||
|
|
||||||
## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03
|
## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
||||||
|
|
||||||
|
## Rust
|
||||||
You'll need to have the stable Rust toolchain installed in order to develop Managarr.
|
You'll need to have the stable Rust toolchain installed in order to develop Managarr.
|
||||||
|
|
||||||
The Rust toolchain (stable) can be installed via rustup using the following command:
|
The Rust toolchain (stable) can be installed via rustup using the following command:
|
||||||
@@ -11,6 +12,37 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|||||||
|
|
||||||
This will install `rustup`, `rustc` and `cargo`. For more information, refer to the [official Rust installation documentation](https://www.rust-lang.org/tools/install).
|
This will install `rustup`, `rustc` and `cargo`. For more information, refer to the [official Rust installation documentation](https://www.rust-lang.org/tools/install).
|
||||||
|
|
||||||
|
## Commitizen
|
||||||
|
[Commitizen](https://github.com/commitizen-tools/commitizen?tab=readme-ov-file) is a nifty tool that helps us write better commit messages. It ensures that our
|
||||||
|
commits have a consistent style and makes it easier to generate CHANGELOGS. Additionally,
|
||||||
|
Commitizen is used to run pre-commit checks to enforce style constraints.
|
||||||
|
|
||||||
|
To install `commitizen` and the `pre-commit` prerequisite, run the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python3 -m pip install commitizen pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commitizen Quick Guide
|
||||||
|
To see an example commit to get an idea for the Commitizen style, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cz example
|
||||||
|
```
|
||||||
|
|
||||||
|
To see the allowed types of commits and their descriptions, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cz info
|
||||||
|
```
|
||||||
|
|
||||||
|
If you'd like to create a commit using Commitizen with an interactive prompt to help you get
|
||||||
|
comfortable with the style, use:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cz commit
|
||||||
|
```
|
||||||
|
|
||||||
## Setup workspace
|
## Setup workspace
|
||||||
|
|
||||||
1. Clone this repo
|
1. Clone this repo
|
||||||
|
|||||||
Generated
+741
-320
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "managarr"
|
name = "managarr"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||||
description = "A TUI and CLI to manage your Servarrs"
|
description = "A TUI and CLI to manage your Servarrs"
|
||||||
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
|
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
|
||||||
@@ -15,20 +15,20 @@ exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.68"
|
anyhow = "1.0.68"
|
||||||
backtrace = "0.3.67"
|
backtrace = "0.3.74"
|
||||||
bimap = { version = "0.6.3", features = ["serde"] }
|
bimap = { version = "0.6.3", features = ["serde"] }
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
confy = { version = "0.6.0", default-features = false, features = [
|
confy = { version = "0.6.0", default-features = false, features = [
|
||||||
"yaml_conf",
|
"yaml_conf",
|
||||||
] }
|
] }
|
||||||
crossterm = "0.27.0"
|
crossterm = "0.28.1"
|
||||||
derivative = "2.2.0"
|
derivative = "2.2.0"
|
||||||
human-panic = "1.1.3"
|
human-panic = "2.0.2"
|
||||||
indoc = "2.0.0"
|
indoc = "2.0.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
log4rs = { version = "1.2.0", features = ["file_appender"] }
|
log4rs = { version = "1.2.0", features = ["file_appender"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.11.14", features = ["json"] }
|
reqwest = { version = "0.12.9", 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.214", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
@@ -36,7 +36,7 @@ strum = { version = "0.26.3", features = ["derive"] }
|
|||||||
strum_macros = "0.26.4"
|
strum_macros = "0.26.4"
|
||||||
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.29.0", features = ["all-widgets"] }
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
|
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
|
||||||
clap_complete = "4.5.33"
|
clap_complete = "4.5.33"
|
||||||
@@ -45,13 +45,15 @@ ctrlc = "3.4.5"
|
|||||||
colored = "2.1.0"
|
colored = "2.1.0"
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
dirs-next = "2.0.0"
|
dirs-next = "2.0.0"
|
||||||
|
managarr-tree-widget = "0.24.0"
|
||||||
|
indicatif = "0.17.9"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0.16"
|
assert_cmd = "2.0.16"
|
||||||
mockall = "0.13.0"
|
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.23.0"
|
||||||
|
|
||||||
[dev-dependencies.cargo-husky]
|
[dev-dependencies.cargo-husky]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|||||||
+1
-1
@@ -23,4 +23,4 @@ FROM debian:stable-slim
|
|||||||
# Copy the compiled binary from the builder container
|
# Copy the compiled binary from the builder container
|
||||||
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
|
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/managarr", "--disable-terminal-size-checks" ]
|
ENTRYPOINT [ "/usr/local/bin/managarr" ]
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||

|
|
||||||

|

|
||||||

|

|
||||||
[](https://crates.io/crates/managarr)
|
[](https://crates.io/crates/managarr)
|
||||||

|

|
||||||
[](https://codecov.io/gh/Dark-Alex-17/managarr)
|
[](https://codecov.io/gh/Dark-Alex-17/managarr)
|
||||||
[](https://github.com/Dark-Alex-17/managarr/releases)
|
|
||||||

|

|
||||||
|
|
||||||
Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust!
|
Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust!
|
||||||
@@ -48,34 +46,64 @@ cargo install --locked managarr
|
|||||||
### Docker
|
### Docker
|
||||||
Run Managarr as a docker container by mounting your `config.yml` file to `/root/.config/managarr/config.yml`. For example:
|
Run Managarr as a docker container by mounting your `config.yml` file to `/root/.config/managarr/config.yml`. For example:
|
||||||
```shell
|
```shell
|
||||||
docker run --rm -it -v ~/.config/managarr:/root/.config/managarr darkalex17/managarr
|
docker run --rm -it -v ~/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command.
|
You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command.
|
||||||
|
|
||||||
|
Please note that you will need to create and popular your configuration file first before starting the container. Otherwise, the container will fail to start.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
Key:
|
||||||
|
|
||||||
|
| Symbol | Status |
|
||||||
|
|--------------------|-----------|
|
||||||
|
| :white_check_mark: | Supported |
|
||||||
|
| :x: | Missing |
|
||||||
|
| :clock3: | Planned |
|
||||||
|
| :no_entry_sign: | Won't Add |
|
||||||
|
|
||||||
### Radarr
|
### Radarr
|
||||||
|
|
||||||
- [x] View your library, downloads, collections, and blocklist
|
| TUI | CLI | Feature |
|
||||||
- [x] View details of a specific movie including description, history, downloaded file info, or the credits
|
|--------------------|--------------------|----------------------------------------------------------------------------------------------------------------|
|
||||||
- [x] View details of any collection and the movies in them
|
| :white_check_mark: | :white_check_mark: | View your library, downloads, collections, and blocklist |
|
||||||
- [x] View your host and security configs from the CLI to programmatically fetch the API token, among other settings
|
| :white_check_mark: | :white_check_mark: | View details of a specific movie including description, history, downloaded file info, or the credits |
|
||||||
- [x] Search your library or collections
|
| :white_check_mark: | :white_check_mark: | View details of any collection and the movies in them |
|
||||||
- [x] Add movies to your library
|
| :no_entry_sign: | :white_check_mark: | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||||
- [x] Delete movies, downloads, and indexers
|
| :white_check_mark: | :white_check_mark: | Search your library or collections |
|
||||||
- [x] Trigger automatic searches for movies
|
| :white_check_mark: | :white_check_mark: | Add movies to your library |
|
||||||
- [x] Trigger refresh and disk scan for movies, downloads, and collections
|
| :white_check_mark: | :white_check_mark: | Delete movies, downloads, and indexers |
|
||||||
- [x] Manually search for movies
|
| :white_check_mark: | :white_check_mark: | Trigger automatic searches for movies |
|
||||||
- [x] Edit your movies, collections, and indexers
|
| :white_check_mark: | :white_check_mark: | Trigger refresh and disk scan for movies, downloads, and collections |
|
||||||
- [x] Manage your tags
|
| :white_check_mark: | :white_check_mark: | Manually search for movies |
|
||||||
- [x] Manage your root folders
|
| :white_check_mark: | :white_check_mark: | Edit your movies, collections, and indexers |
|
||||||
- [x] Manage your blocklist
|
| :white_check_mark: | :white_check_mark: | Manage your tags |
|
||||||
- [x] View and browse logs, tasks, events queues, and updates
|
| :white_check_mark: | :white_check_mark: | Manage your root folders |
|
||||||
- [x] Manually trigger scheduled tasks
|
| :white_check_mark: | :white_check_mark: | Manage your blocklist |
|
||||||
|
| :white_check_mark: | :white_check_mark: | View and browse logs, tasks, events queues, and updates |
|
||||||
|
| :white_check_mark: | :white_check_mark: | Manually trigger scheduled tasks |
|
||||||
|
|
||||||
### Sonarr
|
### Sonarr
|
||||||
- [ ] Support for Sonarr
|
|
||||||
|
| TUI | CLI | Feature |
|
||||||
|
|----------|--------------------|--------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| :clock3: | :white_check_mark: | View your library, downloads, blocklist, episodes |
|
||||||
|
| :clock3: | :white_check_mark: | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
|
||||||
|
| :clock3: | :white_check_mark: | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||||
|
| :clock3: | :white_check_mark: | Search your library |
|
||||||
|
| :clock3: | :white_check_mark: | Add series to your library |
|
||||||
|
| :clock3: | :white_check_mark: | Delete series, downloads, indexers, root folders, and episode files |
|
||||||
|
| :clock3: | :white_check_mark: | Mark history events as failed |
|
||||||
|
| :clock3: | :white_check_mark: | Trigger automatic searches for series, seasons, or episodes |
|
||||||
|
| :clock3: | :white_check_mark: | Trigger refresh and disk scan for series and downloads |
|
||||||
|
| :clock3: | :white_check_mark: | Manually search for series, seasons, or episodes |
|
||||||
|
| :clock3: | :white_check_mark: | Edit your series and indexers |
|
||||||
|
| :clock3: | :white_check_mark: | Manage your tags |
|
||||||
|
| :clock3: | :white_check_mark: | Manage your root folders |
|
||||||
|
| :clock3: | :white_check_mark: | Manage your blocklist |
|
||||||
|
| :clock3: | :white_check_mark: | View and browse logs, tasks, events queues, and updates |
|
||||||
|
| :clock3: | :white_check_mark: | Manually trigger scheduled tasks |
|
||||||
|
|
||||||
### Readarr
|
### Readarr
|
||||||
|
|
||||||
@@ -107,13 +135,13 @@ Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your
|
|||||||
All management features available in the TUI are also available in the CLI. However, the CLI is
|
All management features available in the TUI are also available in the CLI. However, the CLI is
|
||||||
equipped with additional features to allow for more advanced usage and automation.
|
equipped with additional features to allow for more advanced usage and automation.
|
||||||
|
|
||||||
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.
|
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 Radarr library.
|
||||||
|
|
||||||
To see all available commands, simply run `managarr --help`:
|
To see all available commands, simply run `managarr --help`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ managarr --help
|
$ managarr --help
|
||||||
managarr 0.1.5
|
managarr 0.3.0
|
||||||
Alex Clarke <alex.j.tusa@gmail.com>
|
Alex Clarke <alex.j.tusa@gmail.com>
|
||||||
|
|
||||||
A TUI and CLI to manage your Servarrs
|
A TUI and CLI to manage your Servarrs
|
||||||
@@ -122,43 +150,48 @@ Usage: managarr [OPTIONS] [COMMAND]
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
radarr Commands for manging your Radarr instance
|
radarr Commands for manging your Radarr instance
|
||||||
|
sonarr Commands for manging your Sonarr instance
|
||||||
completions Generate shell completions for the Managarr CLI
|
completions Generate shell completions for the Managarr CLI
|
||||||
|
tail-logs Tail Managarr logs
|
||||||
help Print this message or the help of the given subcommand(s)
|
help Print this message or the help of the given subcommand(s)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--config <CONFIG> The Managarr configuration file to use
|
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||||
-h, --help Print help
|
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||||
-V, --version Print version
|
-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:
|
All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ managarr radarr --help
|
$ managarr sonarr --help
|
||||||
Commands for manging your Radarr instance
|
Commands for manging your Sonarr instance
|
||||||
|
|
||||||
Usage: managarr radarr [OPTIONS] <COMMAND>
|
Usage: managarr sonarr [OPTIONS] <COMMAND>
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
add Commands to add or create new resources within your Radarr instance
|
add Commands to add or create new resources within your Sonarr instance
|
||||||
delete Commands to delete resources from your Radarr instance
|
delete Commands to delete resources from your Sonarr instance
|
||||||
edit Commands to edit resources in your Radarr instance
|
edit Commands to edit resources in your Sonarr instance
|
||||||
get Commands to fetch details of the resources in your Radarr instance
|
get Commands to fetch details of the resources in your Sonarr instance
|
||||||
list Commands to list attributes from your Radarr instance
|
download Commands to download releases in your Sonarr instance
|
||||||
refresh Commands to refresh the data in your Radarr instance
|
list Commands to list attributes from your Sonarr instance
|
||||||
clear-blocklist Clear the blocklist
|
refresh Commands to refresh the data in your Sonarr instance
|
||||||
download-release Manually download the given release for the specified movie ID
|
manual-search Commands to manually search for releases
|
||||||
manual-search Trigger a manual search of releases for the movie with the given ID
|
trigger-automatic-search Commands to trigger automatic searches for releases of different resources in your Sonarr instance
|
||||||
search-new-movie Search for a new film to add to Radarr
|
clear-blocklist Clear the blocklist
|
||||||
start-task Start the specified Radarr task
|
mark-history-item-as-failed Mark the Sonarr history item with the given ID as 'failed'
|
||||||
test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'
|
search-new-series Search for a new series to add to Sonarr
|
||||||
test-all-indexers Test all indexers
|
start-task Start the specified Sonarr task
|
||||||
trigger-automatic-search Trigger an automatic search for the movie with the specified ID
|
test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'
|
||||||
help Print this message or the help of the given subcommand(s)
|
test-all-indexers Test all Sonarr indexers
|
||||||
|
help Print this message or the help of the given subcommand(s)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--config <CONFIG> The Managarr configuration file to use
|
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||||
-h, --help Print help
|
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||||
|
-h, --help Print help
|
||||||
```
|
```
|
||||||
|
|
||||||
**Pro 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:
|
**Pro 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:
|
||||||
@@ -172,7 +205,7 @@ $ managarr radarr list movies | jq '.[] | select(.title == "Ad Astra") | .id'
|
|||||||
Managarr assumes reasonable defaults to connect to each service (i.e. Radarr is on localhost:7878),
|
Managarr assumes reasonable defaults to connect to each service (i.e. Radarr is on localhost:7878),
|
||||||
but all servers will require you to input the API token.
|
but all servers will require you to input the API token.
|
||||||
|
|
||||||
The configuration file is located somewhere different for each OS
|
The configuration file is located somewhere different for each OS.
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
```
|
```
|
||||||
@@ -201,53 +234,48 @@ managarr --config /path/to/config.yml
|
|||||||
### Example Configuration:
|
### Example Configuration:
|
||||||
```yaml
|
```yaml
|
||||||
radarr:
|
radarr:
|
||||||
host: 127.0.0.1
|
host: 192.168.0.78
|
||||||
port: 7878
|
port: 7878
|
||||||
api_token: someApiToken1234567890
|
api_token: someApiToken1234567890
|
||||||
use_ssl: true
|
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
|
||||||
ssl_cert_path: /path/to/radarr.crt
|
|
||||||
sonarr:
|
sonarr:
|
||||||
host: 127.0.0.1
|
uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
|
||||||
port: 8989
|
|
||||||
api_token: someApiToken1234567890
|
api_token: someApiToken1234567890
|
||||||
readarr:
|
readarr:
|
||||||
host: 127.0.0.1
|
host: 192.168.0.87
|
||||||
port: 8787
|
port: 8787
|
||||||
api_token: someApiToken1234567890
|
api_token: someApiToken1234567890
|
||||||
use_ssl: false
|
|
||||||
lidarr:
|
lidarr:
|
||||||
host: 127.0.0.1
|
host: 192.168.0.86
|
||||||
port: 8686
|
port: 8686
|
||||||
api_token: someApiToken1234567890
|
api_token: someApiToken1234567890
|
||||||
use_ssl: false
|
|
||||||
whisparr:
|
whisparr:
|
||||||
host: 127.0.0.1
|
host: 192.168.0.69
|
||||||
port: 6969
|
port: 6969
|
||||||
api_token: someApiToken1234567890
|
api_token: someApiToken1234567890
|
||||||
use_ssl: false
|
ssl_cert_path: /path/to/whisparr.crt
|
||||||
bazarr:
|
bazarr:
|
||||||
host: 127.0.0.1
|
host: 192.168.0.67
|
||||||
port: 6767
|
port: 6767
|
||||||
api_token: someApiToken1234567890
|
api_token: someApiToken1234567890
|
||||||
use_ssl: false
|
|
||||||
prowlarr:
|
prowlarr:
|
||||||
host: 127.0.0.1
|
host: 192.168.0.96
|
||||||
port: 9696
|
port: 9696
|
||||||
api_token: someApiToken1234567890
|
api_token: someApiToken1234567890
|
||||||
use_ssl: false
|
|
||||||
tautulli:
|
tautulli:
|
||||||
host: 127.0.0.1
|
host: 192.168.0.81
|
||||||
port: 8181
|
port: 8181
|
||||||
api_token: someApiToken1234567890
|
api_token: someApiToken1234567890
|
||||||
use_ssl: false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
Managarr supports using environment variables on startup so you don't have to always specify certain flags:
|
Managarr supports using environment variables on startup so you don't have to always specify certain flags:
|
||||||
|
|
||||||
| Variable | Description | Equivalent Flag |
|
| Variable | Description | Equivalent Flag |
|
||||||
| --------------------------------------- | -------------------------------- | -------------------------------- |
|
|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
|
||||||
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
|
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
|
||||||
|
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
|
||||||
|
|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
|
||||||
|
|
||||||
## Track My Progress for the Beta release (With Sonarr Support!)
|
## Track My Progress for the Beta release (With Sonarr Support!)
|
||||||
Progress for the beta release can be followed on my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr)
|
Progress for the beta release can be followed on my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr)
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "managarr",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
+62
-17
@@ -1,13 +1,14 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::assert_eq;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
|
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
|
||||||
use crate::app::{App, Data, RadarrConfig, DEFAULT_ROUTE};
|
use crate::app::{App, AppConfig, Data, ServarrConfig, DEFAULT_ROUTE};
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||||
use crate::models::{HorizontallyScrollableText, Route, TabRoute};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||||
|
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
use crate::network::NetworkEvent;
|
use crate::network::NetworkEvent;
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
TabRoute {
|
TabRoute {
|
||||||
title: "Sonarr",
|
title: "Sonarr",
|
||||||
route: Route::Sonarr,
|
route: ActiveSonarrBlock::Series.into(),
|
||||||
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
|
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
|
||||||
contextual_help: None,
|
contextual_help: None,
|
||||||
},
|
},
|
||||||
@@ -47,6 +48,7 @@ mod tests {
|
|||||||
assert!(!app.is_routing);
|
assert!(!app.is_routing);
|
||||||
assert!(!app.should_refresh);
|
assert!(!app.should_refresh);
|
||||||
assert!(!app.should_ignore_quit_key);
|
assert!(!app.should_ignore_quit_key);
|
||||||
|
assert!(!app.cli_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -87,7 +89,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_reset_cancellation_token() {
|
fn test_reset_cancellation_token() {
|
||||||
let mut app = App::default();
|
let mut app = App {
|
||||||
|
is_loading: true,
|
||||||
|
should_refresh: false,
|
||||||
|
..App::default()
|
||||||
|
};
|
||||||
app.cancellation_token.cancel();
|
app.cancellation_token.cancel();
|
||||||
|
|
||||||
assert!(app.cancellation_token.is_cancelled());
|
assert!(app.cancellation_token.is_cancelled());
|
||||||
@@ -96,6 +102,8 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!app.cancellation_token.is_cancelled());
|
assert!(!app.cancellation_token.is_cancelled());
|
||||||
assert!(!new_token.is_cancelled());
|
assert!(!new_token.is_cancelled());
|
||||||
|
assert!(!app.is_loading);
|
||||||
|
assert!(app.should_refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -120,6 +128,10 @@ mod tests {
|
|||||||
version: "test".to_owned(),
|
version: "test".to_owned(),
|
||||||
..RadarrData::default()
|
..RadarrData::default()
|
||||||
},
|
},
|
||||||
|
sonarr_data: SonarrData {
|
||||||
|
version: "test".to_owned(),
|
||||||
|
..SonarrData::default()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
..App::default()
|
..App::default()
|
||||||
};
|
};
|
||||||
@@ -129,6 +141,7 @@ mod tests {
|
|||||||
assert_eq!(app.tick_count, 0);
|
assert_eq!(app.tick_count, 0);
|
||||||
assert_eq!(app.error, HorizontallyScrollableText::default());
|
assert_eq!(app.error, HorizontallyScrollableText::default());
|
||||||
assert!(app.data.radarr_data.version.is_empty());
|
assert!(app.data.radarr_data.version.is_empty());
|
||||||
|
assert!(app.data.sonarr_data.version.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -145,6 +158,29 @@ mod tests {
|
|||||||
assert_eq!(app.error.text, test_string);
|
assert_eq!(app.error.text, test_string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_dispatch_network_event() {
|
||||||
|
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
|
||||||
|
|
||||||
|
let mut app = App {
|
||||||
|
tick_until_poll: 2,
|
||||||
|
network_tx: Some(sync_network_tx),
|
||||||
|
..App::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(app.tick_count, 0);
|
||||||
|
|
||||||
|
app
|
||||||
|
.dispatch_network_event(RadarrEvent::GetStatus.into())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sync_network_rx.recv().await.unwrap(),
|
||||||
|
RadarrEvent::GetStatus.into()
|
||||||
|
);
|
||||||
|
assert_eq!(app.tick_count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_on_tick_first_render() {
|
async fn test_on_tick_first_render() {
|
||||||
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
|
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
|
||||||
@@ -158,6 +194,7 @@ mod tests {
|
|||||||
assert_eq!(app.tick_count, 0);
|
assert_eq!(app.tick_count, 0);
|
||||||
|
|
||||||
app.on_tick(true).await;
|
app.on_tick(true).await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
RadarrEvent::GetQualityProfiles.into()
|
RadarrEvent::GetQualityProfiles.into()
|
||||||
@@ -172,7 +209,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
RadarrEvent::GetOverview.into()
|
RadarrEvent::GetDownloads.into()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sync_network_rx.recv().await.unwrap(),
|
||||||
|
RadarrEvent::GetDiskSpace.into()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
@@ -182,10 +223,6 @@ mod tests {
|
|||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
RadarrEvent::GetMovies.into()
|
RadarrEvent::GetMovies.into()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
sync_network_rx.recv().await.unwrap(),
|
|
||||||
RadarrEvent::GetDownloads.into()
|
|
||||||
);
|
|
||||||
assert!(!app.is_routing);
|
assert!(!app.is_routing);
|
||||||
assert!(!app.should_refresh);
|
assert!(!app.should_refresh);
|
||||||
assert_eq!(app.tick_count, 1);
|
assert_eq!(app.tick_count, 1);
|
||||||
@@ -218,13 +255,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_config_default() {
|
fn test_app_config_default() {
|
||||||
let radarr_config = RadarrConfig::default();
|
let app_config = AppConfig::default();
|
||||||
|
|
||||||
assert_str_eq!(radarr_config.host, "localhost");
|
assert!(app_config.radarr.is_none());
|
||||||
assert_eq!(radarr_config.port, Some(7878));
|
assert!(app_config.sonarr.is_none());
|
||||||
assert!(radarr_config.api_token.is_empty());
|
}
|
||||||
assert!(!radarr_config.use_ssl);
|
|
||||||
assert_eq!(radarr_config.ssl_cert_path, None);
|
#[test]
|
||||||
|
fn test_servarr_config_default() {
|
||||||
|
let servarr_config = ServarrConfig::default();
|
||||||
|
|
||||||
|
assert_eq!(servarr_config.host, Some("localhost".to_string()));
|
||||||
|
assert_eq!(servarr_config.port, None);
|
||||||
|
assert_eq!(servarr_config.uri, None);
|
||||||
|
assert!(servarr_config.api_token.is_empty());
|
||||||
|
assert_eq!(servarr_config.ssl_cert_path, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-14
@@ -1,11 +1,16 @@
|
|||||||
|
use std::process;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use colored::Colorize;
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
|
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
|
||||||
|
use crate::cli::Command;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||||
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||||
use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
|
use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
|
||||||
use crate::network::NetworkEvent;
|
use crate::network::NetworkEvent;
|
||||||
|
|
||||||
@@ -32,6 +37,7 @@ pub struct App<'a> {
|
|||||||
pub is_loading: bool,
|
pub is_loading: bool,
|
||||||
pub should_refresh: bool,
|
pub should_refresh: bool,
|
||||||
pub should_ignore_quit_key: bool,
|
pub should_ignore_quit_key: bool,
|
||||||
|
pub cli_mode: bool,
|
||||||
pub config: AppConfig,
|
pub config: AppConfig,
|
||||||
pub data: Data<'a>,
|
pub data: Data<'a>,
|
||||||
}
|
}
|
||||||
@@ -53,7 +59,10 @@ impl<'a> App<'a> {
|
|||||||
pub async fn dispatch_network_event(&mut self, action: NetworkEvent) {
|
pub async fn dispatch_network_event(&mut self, action: NetworkEvent) {
|
||||||
debug!("Dispatching network event: {action:?}");
|
debug!("Dispatching network event: {action:?}");
|
||||||
|
|
||||||
self.is_loading = true;
|
if !self.should_refresh {
|
||||||
|
self.is_loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(network_tx) = &self.network_tx {
|
if let Some(network_tx) = &self.network_tx {
|
||||||
if let Err(e) = network_tx.send(action).await {
|
if let Err(e) = network_tx.send(action).await {
|
||||||
self.is_loading = false;
|
self.is_loading = false;
|
||||||
@@ -110,6 +119,8 @@ impl<'a> App<'a> {
|
|||||||
|
|
||||||
pub fn reset_cancellation_token(&mut self) -> CancellationToken {
|
pub fn reset_cancellation_token(&mut self) -> CancellationToken {
|
||||||
self.cancellation_token = CancellationToken::new();
|
self.cancellation_token = CancellationToken::new();
|
||||||
|
self.should_refresh = true;
|
||||||
|
self.is_loading = false;
|
||||||
|
|
||||||
self.cancellation_token.clone()
|
self.cancellation_token.clone()
|
||||||
}
|
}
|
||||||
@@ -143,7 +154,7 @@ impl<'a> Default for App<'a> {
|
|||||||
},
|
},
|
||||||
TabRoute {
|
TabRoute {
|
||||||
title: "Sonarr",
|
title: "Sonarr",
|
||||||
route: Route::Sonarr,
|
route: ActiveSonarrBlock::Series.into(),
|
||||||
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
|
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
|
||||||
contextual_help: None,
|
contextual_help: None,
|
||||||
},
|
},
|
||||||
@@ -155,6 +166,7 @@ impl<'a> Default for App<'a> {
|
|||||||
is_routing: false,
|
is_routing: false,
|
||||||
should_refresh: false,
|
should_refresh: false,
|
||||||
should_ignore_quit_key: false,
|
should_ignore_quit_key: false,
|
||||||
|
cli_mode: false,
|
||||||
config: AppConfig::default(),
|
config: AppConfig::default(),
|
||||||
data: Data::default(),
|
data: Data::default(),
|
||||||
}
|
}
|
||||||
@@ -164,31 +176,78 @@ impl<'a> Default for App<'a> {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Data<'a> {
|
pub struct Data<'a> {
|
||||||
pub radarr_data: RadarrData<'a>,
|
pub radarr_data: RadarrData<'a>,
|
||||||
|
pub sonarr_data: SonarrData,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Default)]
|
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub radarr: RadarrConfig,
|
pub radarr: Option<ServarrConfig>,
|
||||||
|
pub sonarr: Option<ServarrConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
impl AppConfig {
|
||||||
pub struct RadarrConfig {
|
pub fn validate(&self) {
|
||||||
pub host: String,
|
if let Some(radarr_config) = &self.radarr {
|
||||||
|
radarr_config.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sonarr_config) = &self.sonarr {
|
||||||
|
sonarr_config.validate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_config_present_for_cli(&self, command: &Command) {
|
||||||
|
let msg = |servarr: &str| {
|
||||||
|
log_and_print_error(format!(
|
||||||
|
"{} configuration missing; Unable to run any {} commands.",
|
||||||
|
servarr, servarr
|
||||||
|
))
|
||||||
|
};
|
||||||
|
match command {
|
||||||
|
Command::Radarr(_) if self.radarr.is_none() => {
|
||||||
|
msg("Radarr");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
Command::Sonarr(_) if self.sonarr.is_none() => {
|
||||||
|
msg("Sonarr");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct ServarrConfig {
|
||||||
|
pub host: Option<String>,
|
||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
|
pub uri: Option<String>,
|
||||||
pub api_token: String,
|
pub api_token: String,
|
||||||
#[serde(default)]
|
|
||||||
pub use_ssl: bool,
|
|
||||||
pub ssl_cert_path: Option<String>,
|
pub ssl_cert_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RadarrConfig {
|
impl ServarrConfig {
|
||||||
|
fn validate(&self) {
|
||||||
|
if self.host.is_none() && self.uri.is_none() {
|
||||||
|
log_and_print_error("'host' or 'uri' is required for configuration".to_owned());
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServarrConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
RadarrConfig {
|
ServarrConfig {
|
||||||
host: "localhost".to_string(),
|
host: Some("localhost".to_string()),
|
||||||
port: Some(7878),
|
port: None,
|
||||||
|
uri: None,
|
||||||
api_token: "".to_string(),
|
api_token: "".to_string(),
|
||||||
use_ssl: false,
|
|
||||||
ssl_cert_path: None,
|
ssl_cert_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn log_and_print_error(error: String) {
|
||||||
|
error!("{}", error);
|
||||||
|
eprintln!("error: {}", error.red());
|
||||||
|
}
|
||||||
|
|||||||
+12
-19
@@ -142,35 +142,22 @@ impl<'a> App<'a> {
|
|||||||
is_first_render: bool,
|
is_first_render: bool,
|
||||||
) {
|
) {
|
||||||
if is_first_render {
|
if is_first_render {
|
||||||
self
|
self.refresh_metadata().await;
|
||||||
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
|
|
||||||
.await;
|
|
||||||
self
|
|
||||||
.dispatch_network_event(RadarrEvent::GetTags.into())
|
|
||||||
.await;
|
|
||||||
self
|
|
||||||
.dispatch_network_event(RadarrEvent::GetRootFolders.into())
|
|
||||||
.await;
|
|
||||||
self
|
|
||||||
.dispatch_network_event(RadarrEvent::GetOverview.into())
|
|
||||||
.await;
|
|
||||||
self
|
|
||||||
.dispatch_network_event(RadarrEvent::GetStatus.into())
|
|
||||||
.await;
|
|
||||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.should_refresh {
|
if self.should_refresh {
|
||||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||||
|
self.refresh_metadata().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.is_routing {
|
if self.is_routing {
|
||||||
if self.is_loading && !self.should_refresh {
|
if !self.should_refresh {
|
||||||
self.cancellation_token.cancel();
|
self.cancellation_token.cancel();
|
||||||
|
} else {
|
||||||
|
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||||
|
self.refresh_metadata().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
|
||||||
self.refresh_metadata().await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tick_count % self.tick_until_poll == 0 {
|
if self.tick_count % self.tick_until_poll == 0 {
|
||||||
@@ -191,6 +178,12 @@ impl<'a> App<'a> {
|
|||||||
self
|
self
|
||||||
.dispatch_network_event(RadarrEvent::GetDownloads.into())
|
.dispatch_network_event(RadarrEvent::GetDownloads.into())
|
||||||
.await;
|
.await;
|
||||||
|
self
|
||||||
|
.dispatch_network_event(RadarrEvent::GetDiskSpace.into())
|
||||||
|
.await;
|
||||||
|
self
|
||||||
|
.dispatch_network_event(RadarrEvent::GetStatus.into())
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn populate_movie_collection_table(&mut self) {
|
async fn populate_movie_collection_table(&mut self) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::app::radarr::ActiveRadarrBlock;
|
use crate::app::radarr::ActiveRadarrBlock;
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::radarr_models::{Collection, CollectionMovie, Credit, Release};
|
use crate::models::radarr_models::{Collection, CollectionMovie, Credit, RadarrRelease};
|
||||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||||
|
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
@@ -430,7 +430,7 @@ mod tests {
|
|||||||
let mut movie_details_modal = MovieDetailsModal::default();
|
let mut movie_details_modal = MovieDetailsModal::default();
|
||||||
movie_details_modal
|
movie_details_modal
|
||||||
.movie_releases
|
.movie_releases
|
||||||
.set_items(vec![Release::default()]);
|
.set_items(vec![RadarrRelease::default()]);
|
||||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||||
|
|
||||||
app
|
app
|
||||||
@@ -508,6 +508,14 @@ mod tests {
|
|||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
RadarrEvent::GetDownloads.into()
|
RadarrEvent::GetDownloads.into()
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sync_network_rx.recv().await.unwrap(),
|
||||||
|
RadarrEvent::GetDiskSpace.into()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sync_network_rx.recv().await.unwrap(),
|
||||||
|
RadarrEvent::GetStatus.into()
|
||||||
|
);
|
||||||
assert!(app.is_loading);
|
assert!(app.is_loading);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,16 +539,16 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
RadarrEvent::GetOverview.into()
|
RadarrEvent::GetDownloads.into()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sync_network_rx.recv().await.unwrap(),
|
||||||
|
RadarrEvent::GetDiskSpace.into()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
RadarrEvent::GetStatus.into()
|
RadarrEvent::GetStatus.into()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
sync_network_rx.recv().await.unwrap(),
|
|
||||||
RadarrEvent::GetDownloads.into()
|
|
||||||
);
|
|
||||||
assert!(app.is_loading);
|
assert!(app.is_loading);
|
||||||
assert!(!app.data.radarr_data.prompt_confirm);
|
assert!(!app.data.radarr_data.prompt_confirm);
|
||||||
}
|
}
|
||||||
@@ -549,6 +557,7 @@ mod tests {
|
|||||||
async fn test_radarr_on_tick_routing() {
|
async fn test_radarr_on_tick_routing() {
|
||||||
let (mut app, mut sync_network_rx) = construct_app_unit();
|
let (mut app, mut sync_network_rx) = construct_app_unit();
|
||||||
app.is_routing = true;
|
app.is_routing = true;
|
||||||
|
app.should_refresh = true;
|
||||||
|
|
||||||
app
|
app
|
||||||
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
|
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
|
||||||
@@ -574,43 +583,19 @@ mod tests {
|
|||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
RadarrEvent::GetDownloads.into()
|
RadarrEvent::GetDownloads.into()
|
||||||
);
|
);
|
||||||
assert!(app.is_loading);
|
|
||||||
assert!(!app.data.radarr_data.prompt_confirm);
|
assert!(!app.data.radarr_data.prompt_confirm);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
|
async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
|
||||||
let (mut app, mut sync_network_rx) = construct_app_unit();
|
let (mut app, _) = construct_app_unit();
|
||||||
app.is_routing = true;
|
app.is_routing = true;
|
||||||
app.is_loading = true;
|
|
||||||
app.should_refresh = false;
|
app.should_refresh = false;
|
||||||
|
|
||||||
app
|
app
|
||||||
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
|
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
sync_network_rx.recv().await.unwrap(),
|
|
||||||
RadarrEvent::GetDownloads.into()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
sync_network_rx.recv().await.unwrap(),
|
|
||||||
RadarrEvent::GetQualityProfiles.into()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
sync_network_rx.recv().await.unwrap(),
|
|
||||||
RadarrEvent::GetTags.into()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
sync_network_rx.recv().await.unwrap(),
|
|
||||||
RadarrEvent::GetRootFolders.into()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
sync_network_rx.recv().await.unwrap(),
|
|
||||||
RadarrEvent::GetDownloads.into()
|
|
||||||
);
|
|
||||||
assert!(app.is_loading);
|
|
||||||
assert!(!app.data.radarr_data.prompt_confirm);
|
|
||||||
assert!(app.cancellation_token.is_cancelled());
|
assert!(app.cancellation_token.is_cancelled());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,7 +612,6 @@ mod tests {
|
|||||||
sync_network_rx.recv().await.unwrap(),
|
sync_network_rx.recv().await.unwrap(),
|
||||||
RadarrEvent::GetDownloads.into()
|
RadarrEvent::GetDownloads.into()
|
||||||
);
|
);
|
||||||
assert!(app.is_loading);
|
|
||||||
assert!(app.should_refresh);
|
assert!(app.should_refresh);
|
||||||
assert!(!app.data.radarr_data.prompt_confirm);
|
assert!(!app.data.radarr_data.prompt_confirm);
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-10
@@ -10,19 +10,28 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand},
|
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand},
|
||||||
models::{
|
models::{
|
||||||
radarr_models::{BlocklistItem, BlocklistResponse, RadarrSerdeable},
|
radarr_models::{
|
||||||
|
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
|
||||||
|
RadarrSerdeable,
|
||||||
|
},
|
||||||
|
sonarr_models::{
|
||||||
|
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
|
||||||
|
SonarrSerdeable,
|
||||||
|
},
|
||||||
Serdeable,
|
Serdeable,
|
||||||
},
|
},
|
||||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
network::{
|
||||||
|
radarr_network::RadarrEvent, sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent,
|
||||||
|
},
|
||||||
Cli,
|
Cli,
|
||||||
};
|
};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn test_radarr_subcommand_requires_subcommand() {
|
fn test_servarr_subcommand_requires_subcommand(#[values("radarr", "sonarr")] subcommand: &str) {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr"]);
|
let result = Cli::command().try_get_matches_from(["managarr", subcommand]);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -39,6 +48,13 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_subcommand_delegates_to_sonarr() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_completions_requires_argument() {
|
fn test_completions_requires_argument() {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||||
@@ -106,8 +122,8 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.returning(|_| {
|
.returning(|_| {
|
||||||
Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse(
|
Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse(
|
||||||
BlocklistResponse {
|
RadarrBlocklistResponse {
|
||||||
records: vec![BlocklistItem::default()],
|
records: vec![RadarrBlocklistItem::default()],
|
||||||
},
|
},
|
||||||
)))
|
)))
|
||||||
});
|
});
|
||||||
@@ -121,9 +137,40 @@ mod tests {
|
|||||||
)))
|
)))
|
||||||
});
|
});
|
||||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
let claer_blocklist_command = RadarrCommand::ClearBlocklist.into();
|
let clear_blocklist_command = RadarrCommand::ClearBlocklist.into();
|
||||||
|
|
||||||
let result = handle_command(&app_arc, claer_blocklist_command, &mut mock_network).await;
|
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cli_handler_delegates_sonarr_commands_to_the_sonarr_cli_handler() {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(SonarrEvent::GetBlocklist.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::BlocklistResponse(
|
||||||
|
SonarrBlocklistResponse {
|
||||||
|
records: vec![SonarrBlocklistItem::default()],
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(SonarrEvent::ClearBlocklist.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let clear_blocklist_command = SonarrCommand::ClearBlocklist.into();
|
||||||
|
|
||||||
|
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||||
|
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-21
@@ -4,11 +4,13 @@ use anyhow::Result;
|
|||||||
use clap::{command, Subcommand};
|
use clap::{command, Subcommand};
|
||||||
use clap_complete::Shell;
|
use clap_complete::Shell;
|
||||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||||
|
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{app::App, network::NetworkTrait};
|
use crate::{app::App, network::NetworkTrait};
|
||||||
|
|
||||||
pub mod radarr;
|
pub mod radarr;
|
||||||
|
pub mod sonarr;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "cli_tests.rs"]
|
#[path = "cli_tests.rs"]
|
||||||
@@ -19,6 +21,9 @@ pub enum Command {
|
|||||||
#[command(subcommand, about = "Commands for manging your Radarr instance")]
|
#[command(subcommand, about = "Commands for manging your Radarr instance")]
|
||||||
Radarr(RadarrCommand),
|
Radarr(RadarrCommand),
|
||||||
|
|
||||||
|
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
|
||||||
|
Sonarr(SonarrCommand),
|
||||||
|
|
||||||
#[command(
|
#[command(
|
||||||
arg_required_else_help = true,
|
arg_required_else_help = true,
|
||||||
about = "Generate shell completions for the Managarr CLI"
|
about = "Generate shell completions for the Managarr CLI"
|
||||||
@@ -27,24 +32,39 @@ pub enum Command {
|
|||||||
#[arg(value_enum)]
|
#[arg(value_enum)]
|
||||||
shell: Shell,
|
shell: Shell,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[command(about = "Tail Managarr logs")]
|
||||||
|
TailLogs {
|
||||||
|
#[arg(long, help = "Disable colored log output")]
|
||||||
|
no_color: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
|
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
|
||||||
fn with(app: &'a Arc<Mutex<App<'b>>>, command: T, network: &'a mut dyn NetworkTrait) -> Self;
|
fn with(app: &'a Arc<Mutex<App<'b>>>, command: T, network: &'a mut dyn NetworkTrait) -> Self;
|
||||||
async fn handle(self) -> Result<()>;
|
async fn handle(self) -> Result<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_command(
|
pub(crate) async fn handle_command(
|
||||||
app: &Arc<Mutex<App<'_>>>,
|
app: &Arc<Mutex<App<'_>>>,
|
||||||
command: Command,
|
command: Command,
|
||||||
network: &mut dyn NetworkTrait,
|
network: &mut dyn NetworkTrait,
|
||||||
) -> Result<()> {
|
) -> Result<String> {
|
||||||
if let Command::Radarr(radarr_command) = command {
|
let result = match command {
|
||||||
RadarrCliHandler::with(app, radarr_command, network)
|
Command::Radarr(radarr_command) => {
|
||||||
.handle()
|
RadarrCliHandler::with(app, radarr_command, network)
|
||||||
.await?
|
.handle()
|
||||||
}
|
.await?
|
||||||
Ok(())
|
}
|
||||||
|
Command::Sonarr(sonarr_command) => {
|
||||||
|
SonarrCliHandler::with(app, sonarr_command, network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -68,16 +88,3 @@ pub fn mutex_flags_or_default(positive: bool, negative: bool, default_value: boo
|
|||||||
default_value
|
default_value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! execute_network_event {
|
|
||||||
($self:ident, $event:expr) => {
|
|
||||||
let resp = $self.network.handle_network_event($event.into()).await?;
|
|
||||||
let json = serde_json::to_string_pretty(&resp)?;
|
|
||||||
println!("{}", json);
|
|
||||||
};
|
|
||||||
($self:ident, $event:expr, $happy_output:expr) => {
|
|
||||||
$self.network.handle_network_event($event.into()).await?;
|
|
||||||
println!("{}", $happy_output);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ use tokio::sync::Mutex;
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{CliCommandHandler, Command},
|
cli::{CliCommandHandler, Command},
|
||||||
execute_network_event,
|
models::radarr_models::{AddMovieBody, AddMovieOptions, MinimumAvailability, MovieMonitor},
|
||||||
models::radarr_models::{AddMovieBody, AddOptions, MinimumAvailability, Monitor},
|
|
||||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ pub enum RadarrAddCommand {
|
|||||||
default_value_t = MinimumAvailability::default()
|
default_value_t = MinimumAvailability::default()
|
||||||
)]
|
)]
|
||||||
minimum_availability: MinimumAvailability,
|
minimum_availability: MinimumAvailability,
|
||||||
#[arg(long, help = "Should Radarr monitor this film")]
|
#[arg(long, help = "Disable monitoring for this film")]
|
||||||
disable_monitoring: bool,
|
disable_monitoring: bool,
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
@@ -60,9 +59,9 @@ pub enum RadarrAddCommand {
|
|||||||
long,
|
long,
|
||||||
help = "What Radarr should monitor",
|
help = "What Radarr should monitor",
|
||||||
value_enum,
|
value_enum,
|
||||||
default_value_t = Monitor::default()
|
default_value_t = MovieMonitor::default()
|
||||||
)]
|
)]
|
||||||
monitor: Monitor,
|
monitor: MovieMonitor,
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
help = "Tell Radarr to not start a search for this film once it's added to your library"
|
help = "Tell Radarr to not start a search for this film once it's added to your library"
|
||||||
@@ -106,8 +105,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(self) -> Result<()> {
|
async fn handle(self) -> Result<String> {
|
||||||
match self.command {
|
let result = match self.command {
|
||||||
RadarrAddCommand::Movie {
|
RadarrAddCommand::Movie {
|
||||||
tmdb_id,
|
tmdb_id,
|
||||||
root_folder_path,
|
root_folder_path,
|
||||||
@@ -126,24 +125,33 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
|
|||||||
minimum_availability: minimum_availability.to_string(),
|
minimum_availability: minimum_availability.to_string(),
|
||||||
monitored: !disable_monitoring,
|
monitored: !disable_monitoring,
|
||||||
tags,
|
tags,
|
||||||
add_options: AddOptions {
|
add_options: AddMovieOptions {
|
||||||
monitor: monitor.to_string(),
|
monitor: monitor.to_string(),
|
||||||
search_for_movie: !no_search_for_movie,
|
search_for_movie: !no_search_for_movie,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
execute_network_event!(self, RadarrEvent::AddMovie(Some(body)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::AddMovie(Some(body)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrAddCommand::RootFolder { root_folder_path } => {
|
RadarrAddCommand::RootFolder { root_folder_path } => {
|
||||||
execute_network_event!(
|
let resp = self
|
||||||
self,
|
.network
|
||||||
RadarrEvent::AddRootFolder(Some(root_folder_path.clone()))
|
.handle_network_event(RadarrEvent::AddRootFolder(Some(root_folder_path)).into())
|
||||||
);
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrAddCommand::Tag { name } => {
|
RadarrAddCommand::Tag { name } => {
|
||||||
execute_network_event!(self, RadarrEvent::AddTag(name.clone()));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::AddTag(name).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ mod tests {
|
|||||||
radarr::{add_command_handler::RadarrAddCommand, RadarrCommand},
|
radarr::{add_command_handler::RadarrAddCommand, RadarrCommand},
|
||||||
Command,
|
Command,
|
||||||
},
|
},
|
||||||
models::radarr_models::{MinimumAvailability, Monitor},
|
models::radarr_models::{MinimumAvailability, MovieMonitor},
|
||||||
Cli,
|
Cli,
|
||||||
};
|
};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_add_command_from() {
|
fn test_radarr_add_command_from() {
|
||||||
@@ -111,6 +112,8 @@ mod tests {
|
|||||||
"/test",
|
"/test",
|
||||||
"--quality-profile-id",
|
"--quality-profile-id",
|
||||||
"1",
|
"1",
|
||||||
|
"--tmdb-id",
|
||||||
|
"1",
|
||||||
flag,
|
flag,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -187,7 +190,7 @@ mod tests {
|
|||||||
minimum_availability: MinimumAvailability::default(),
|
minimum_availability: MinimumAvailability::default(),
|
||||||
disable_monitoring: false,
|
disable_monitoring: false,
|
||||||
tag: vec![],
|
tag: vec![],
|
||||||
monitor: Monitor::default(),
|
monitor: MovieMonitor::default(),
|
||||||
no_search_for_movie: false,
|
no_search_for_movie: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,7 +222,7 @@ mod tests {
|
|||||||
minimum_availability: MinimumAvailability::default(),
|
minimum_availability: MinimumAvailability::default(),
|
||||||
disable_monitoring: false,
|
disable_monitoring: false,
|
||||||
tag: vec![1, 2],
|
tag: vec![1, 2],
|
||||||
monitor: Monitor::default(),
|
monitor: MovieMonitor::default(),
|
||||||
no_search_for_movie: false,
|
no_search_for_movie: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -255,7 +258,7 @@ mod tests {
|
|||||||
minimum_availability: MinimumAvailability::Released,
|
minimum_availability: MinimumAvailability::Released,
|
||||||
disable_monitoring: true,
|
disable_monitoring: true,
|
||||||
tag: vec![1, 2],
|
tag: vec![1, 2],
|
||||||
monitor: Monitor::MovieAndCollection,
|
monitor: MovieMonitor::MovieAndCollection,
|
||||||
no_search_for_movie: true,
|
no_search_for_movie: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -356,7 +359,7 @@ mod tests {
|
|||||||
app::App,
|
app::App,
|
||||||
cli::{radarr::add_command_handler::RadarrAddCommandHandler, CliCommandHandler},
|
cli::{radarr::add_command_handler::RadarrAddCommandHandler, CliCommandHandler},
|
||||||
models::{
|
models::{
|
||||||
radarr_models::{AddMovieBody, AddOptions, RadarrSerdeable},
|
radarr_models::{AddMovieBody, AddMovieOptions, RadarrSerdeable},
|
||||||
Serdeable,
|
Serdeable,
|
||||||
},
|
},
|
||||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
||||||
@@ -378,7 +381,7 @@ mod tests {
|
|||||||
minimum_availability: "released".to_owned(),
|
minimum_availability: "released".to_owned(),
|
||||||
monitored: false,
|
monitored: false,
|
||||||
tags: vec![1, 2],
|
tags: vec![1, 2],
|
||||||
add_options: AddOptions {
|
add_options: AddMovieOptions {
|
||||||
monitor: "movieAndCollection".to_owned(),
|
monitor: "movieAndCollection".to_owned(),
|
||||||
search_for_movie: false,
|
search_for_movie: false,
|
||||||
},
|
},
|
||||||
@@ -403,7 +406,7 @@ mod tests {
|
|||||||
minimum_availability: MinimumAvailability::Released,
|
minimum_availability: MinimumAvailability::Released,
|
||||||
disable_monitoring: true,
|
disable_monitoring: true,
|
||||||
tag: vec![1, 2],
|
tag: vec![1, 2],
|
||||||
monitor: Monitor::MovieAndCollection,
|
monitor: MovieMonitor::MovieAndCollection,
|
||||||
no_search_for_movie: true,
|
no_search_for_movie: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{CliCommandHandler, Command},
|
cli::{CliCommandHandler, Command},
|
||||||
execute_network_event,
|
|
||||||
models::radarr_models::DeleteMovieParams,
|
models::radarr_models::DeleteMovieParams,
|
||||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||||
};
|
};
|
||||||
@@ -85,19 +84,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(self) -> Result<()> {
|
async fn handle(self) -> Result<String> {
|
||||||
match self.command {
|
let result = match self.command {
|
||||||
RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||||
execute_network_event!(
|
let resp = self
|
||||||
self,
|
.network
|
||||||
RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))
|
.handle_network_event(RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into())
|
||||||
);
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrDeleteCommand::Download { download_id } => {
|
RadarrDeleteCommand::Download { download_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::DeleteDownload(Some(download_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::DeleteDownload(Some(download_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrDeleteCommand::Indexer { indexer_id } => {
|
RadarrDeleteCommand::Indexer { indexer_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::DeleteIndexer(Some(indexer_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::DeleteIndexer(Some(indexer_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrDeleteCommand::Movie {
|
RadarrDeleteCommand::Movie {
|
||||||
movie_id,
|
movie_id,
|
||||||
@@ -109,16 +117,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
|
|||||||
delete_movie_files: delete_files_from_disk,
|
delete_movie_files: delete_files_from_disk,
|
||||||
add_list_exclusion,
|
add_list_exclusion,
|
||||||
};
|
};
|
||||||
execute_network_event!(self, RadarrEvent::DeleteMovie(Some(delete_movie_params)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::DeleteMovie(Some(delete_movie_params)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrDeleteCommand::RootFolder { root_folder_id } => {
|
RadarrDeleteCommand::RootFolder { root_folder_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::DeleteRootFolder(Some(root_folder_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::DeleteRootFolder(Some(root_folder_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrDeleteCommand::Tag { tag_id } => {
|
RadarrDeleteCommand::Tag { tag_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::DeleteTag(tag_id));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::DeleteTag(tag_id).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod tests {
|
|||||||
Cli,
|
Cli,
|
||||||
};
|
};
|
||||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_delete_command_from() {
|
fn test_radarr_delete_command_from() {
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ use tokio::sync::Mutex;
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command},
|
cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command},
|
||||||
execute_network_event,
|
|
||||||
models::{
|
models::{
|
||||||
radarr_models::{
|
radarr_models::{
|
||||||
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
|
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability, RadarrSerdeable,
|
||||||
MinimumAvailability, RadarrSerdeable,
|
|
||||||
},
|
},
|
||||||
|
servarr_models::EditIndexerParams,
|
||||||
Serdeable,
|
Serdeable,
|
||||||
},
|
},
|
||||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||||
@@ -339,8 +338,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(self) -> Result<()> {
|
async fn handle(self) -> Result<String> {
|
||||||
match self.command {
|
let result = match self.command {
|
||||||
RadarrEditCommand::AllIndexerSettings {
|
RadarrEditCommand::AllIndexerSettings {
|
||||||
allow_hardcoded_subs,
|
allow_hardcoded_subs,
|
||||||
disable_allow_hardcoded_subs,
|
disable_allow_hardcoded_subs,
|
||||||
@@ -389,11 +388,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
|||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
};
|
};
|
||||||
execute_network_event!(
|
self
|
||||||
self,
|
.network
|
||||||
RadarrEvent::EditAllIndexerSettings(Some(params)),
|
.handle_network_event(RadarrEvent::EditAllIndexerSettings(Some(params)).into())
|
||||||
"All indexer settings updated"
|
.await?;
|
||||||
);
|
"All indexer settings updated".to_owned()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RadarrEditCommand::Collection {
|
RadarrEditCommand::Collection {
|
||||||
@@ -417,11 +418,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
|||||||
root_folder_path,
|
root_folder_path,
|
||||||
search_on_add: search_on_add_value,
|
search_on_add: search_on_add_value,
|
||||||
};
|
};
|
||||||
execute_network_event!(
|
self
|
||||||
self,
|
.network
|
||||||
RadarrEvent::EditCollection(Some(edit_collection_params)),
|
.handle_network_event(RadarrEvent::EditCollection(Some(edit_collection_params)).into())
|
||||||
"Collection Updated"
|
.await?;
|
||||||
);
|
"Collection updated".to_owned()
|
||||||
}
|
}
|
||||||
RadarrEditCommand::Indexer {
|
RadarrEditCommand::Indexer {
|
||||||
indexer_id,
|
indexer_id,
|
||||||
@@ -458,11 +459,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
|||||||
clear_tags,
|
clear_tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
execute_network_event!(
|
self
|
||||||
self,
|
.network
|
||||||
RadarrEvent::EditIndexer(Some(edit_indexer_params)),
|
.handle_network_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)).into())
|
||||||
"Indexer updated"
|
.await?;
|
||||||
);
|
"Indexer updated".to_owned()
|
||||||
}
|
}
|
||||||
RadarrEditCommand::Movie {
|
RadarrEditCommand::Movie {
|
||||||
movie_id,
|
movie_id,
|
||||||
@@ -485,14 +486,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
|||||||
clear_tags,
|
clear_tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
execute_network_event!(
|
self
|
||||||
self,
|
.network
|
||||||
RadarrEvent::EditMovie(Some(edit_movie_params)),
|
.handle_network_event(RadarrEvent::EditMovie(Some(edit_movie_params)).into())
|
||||||
"Movie updated"
|
.await?;
|
||||||
);
|
"Movie Updated".to_owned()
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod tests {
|
|||||||
Cli,
|
Cli,
|
||||||
};
|
};
|
||||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_edit_command_from() {
|
fn test_radarr_edit_command_from() {
|
||||||
@@ -809,9 +810,10 @@ mod tests {
|
|||||||
},
|
},
|
||||||
models::{
|
models::{
|
||||||
radarr_models::{
|
radarr_models::{
|
||||||
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
|
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability,
|
||||||
MinimumAvailability, RadarrSerdeable,
|
RadarrSerdeable,
|
||||||
},
|
},
|
||||||
|
servarr_models::EditIndexerParams,
|
||||||
Serdeable,
|
Serdeable,
|
||||||
},
|
},
|
||||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{CliCommandHandler, Command},
|
cli::{CliCommandHandler, Command},
|
||||||
execute_network_event,
|
|
||||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,28 +71,52 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(self) -> Result<()> {
|
async fn handle(self) -> Result<String> {
|
||||||
match self.command {
|
let result = match self.command {
|
||||||
RadarrGetCommand::AllIndexerSettings => {
|
RadarrGetCommand::AllIndexerSettings => {
|
||||||
execute_network_event!(self, RadarrEvent::GetAllIndexerSettings);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetAllIndexerSettings.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrGetCommand::HostConfig => {
|
RadarrGetCommand::HostConfig => {
|
||||||
execute_network_event!(self, RadarrEvent::GetHostConfig);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetHostConfig.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrGetCommand::MovieDetails { movie_id } => {
|
RadarrGetCommand::MovieDetails { movie_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::GetMovieDetails(Some(movie_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetMovieDetails(Some(movie_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrGetCommand::MovieHistory { movie_id } => {
|
RadarrGetCommand::MovieHistory { movie_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::GetMovieHistory(Some(movie_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetMovieHistory(Some(movie_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrGetCommand::SecurityConfig => {
|
RadarrGetCommand::SecurityConfig => {
|
||||||
execute_network_event!(self, RadarrEvent::GetSecurityConfig);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetSecurityConfig.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrGetCommand::SystemStatus => {
|
RadarrGetCommand::SystemStatus => {
|
||||||
execute_network_event!(self, RadarrEvent::GetStatus);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetStatus.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod tests {
|
||||||
use clap::error::ErrorKind;
|
use clap::error::ErrorKind;
|
||||||
use clap::CommandFactory;
|
use clap::CommandFactory;
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ mod test {
|
|||||||
use crate::cli::radarr::RadarrCommand;
|
use crate::cli::radarr::RadarrCommand;
|
||||||
use crate::cli::Command;
|
use crate::cli::Command;
|
||||||
use crate::Cli;
|
use crate::Cli;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_get_command_from() {
|
fn test_radarr_get_command_from() {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{CliCommandHandler, Command},
|
cli::{CliCommandHandler, Command},
|
||||||
execute_network_event,
|
|
||||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,6 +24,8 @@ pub enum RadarrListCommand {
|
|||||||
Collections,
|
Collections,
|
||||||
#[command(about = "List all active downloads in Radarr")]
|
#[command(about = "List all active downloads in Radarr")]
|
||||||
Downloads,
|
Downloads,
|
||||||
|
#[command(about = "List disk space details for all provisioned root folders in Radarr")]
|
||||||
|
DiskSpace,
|
||||||
#[command(about = "List all Radarr indexers")]
|
#[command(about = "List all Radarr indexers")]
|
||||||
Indexers,
|
Indexers,
|
||||||
#[command(about = "Fetch Radarr logs")]
|
#[command(about = "Fetch Radarr logs")]
|
||||||
@@ -56,7 +57,7 @@ pub enum RadarrListCommand {
|
|||||||
RootFolders,
|
RootFolders,
|
||||||
#[command(about = "List all Radarr tags")]
|
#[command(about = "List all Radarr tags")]
|
||||||
Tags,
|
Tags,
|
||||||
#[command(about = "List tasks")]
|
#[command(about = "List all Radarr tasks")]
|
||||||
Tasks,
|
Tasks,
|
||||||
#[command(about = "List all Radarr updates")]
|
#[command(about = "List all Radarr updates")]
|
||||||
Updates,
|
Updates,
|
||||||
@@ -87,19 +88,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(self) -> Result<()> {
|
async fn handle(self) -> Result<String> {
|
||||||
match self.command {
|
let result = match self.command {
|
||||||
RadarrListCommand::Blocklist => {
|
RadarrListCommand::Blocklist => {
|
||||||
execute_network_event!(self, RadarrEvent::GetBlocklist);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetBlocklist.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::Collections => {
|
RadarrListCommand::Collections => {
|
||||||
execute_network_event!(self, RadarrEvent::GetCollections);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetCollections.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::Downloads => {
|
RadarrListCommand::Downloads => {
|
||||||
execute_network_event!(self, RadarrEvent::GetDownloads);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetDownloads.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
RadarrListCommand::DiskSpace => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetDiskSpace.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::Indexers => {
|
RadarrListCommand::Indexers => {
|
||||||
execute_network_event!(self, RadarrEvent::GetIndexers);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetIndexers.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::Logs {
|
RadarrListCommand::Logs {
|
||||||
events,
|
events,
|
||||||
@@ -113,39 +137,69 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
|
|||||||
if output_in_log_format {
|
if output_in_log_format {
|
||||||
let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone();
|
let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone();
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&log_lines)?;
|
serde_json::to_string_pretty(&log_lines)?
|
||||||
println!("{}", json);
|
|
||||||
} else {
|
} else {
|
||||||
let json = serde_json::to_string_pretty(&logs)?;
|
serde_json::to_string_pretty(&logs)?
|
||||||
println!("{}", json);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RadarrListCommand::Movies => {
|
RadarrListCommand::Movies => {
|
||||||
execute_network_event!(self, RadarrEvent::GetMovies);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetMovies.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::MovieCredits { movie_id } => {
|
RadarrListCommand::MovieCredits { movie_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::GetMovieCredits(Some(movie_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetMovieCredits(Some(movie_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::QualityProfiles => {
|
RadarrListCommand::QualityProfiles => {
|
||||||
execute_network_event!(self, RadarrEvent::GetQualityProfiles);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetQualityProfiles.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::QueuedEvents => {
|
RadarrListCommand::QueuedEvents => {
|
||||||
execute_network_event!(self, RadarrEvent::GetQueuedEvents);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetQueuedEvents.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::RootFolders => {
|
RadarrListCommand::RootFolders => {
|
||||||
execute_network_event!(self, RadarrEvent::GetRootFolders);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetRootFolders.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::Tags => {
|
RadarrListCommand::Tags => {
|
||||||
execute_network_event!(self, RadarrEvent::GetTags);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetTags.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::Tasks => {
|
RadarrListCommand::Tasks => {
|
||||||
execute_network_event!(self, RadarrEvent::GetTasks);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetTasks.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrListCommand::Updates => {
|
RadarrListCommand::Updates => {
|
||||||
execute_network_event!(self, RadarrEvent::GetUpdates);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetUpdates.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod tests {
|
|||||||
use crate::cli::radarr::RadarrCommand;
|
use crate::cli::radarr::RadarrCommand;
|
||||||
use crate::cli::Command;
|
use crate::cli::Command;
|
||||||
use crate::Cli;
|
use crate::Cli;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_list_command_from() {
|
fn test_radarr_list_command_from() {
|
||||||
@@ -29,6 +30,7 @@ mod tests {
|
|||||||
"blocklist",
|
"blocklist",
|
||||||
"collections",
|
"collections",
|
||||||
"downloads",
|
"downloads",
|
||||||
|
"disk-space",
|
||||||
"indexers",
|
"indexers",
|
||||||
"movies",
|
"movies",
|
||||||
"quality-profiles",
|
"quality-profiles",
|
||||||
@@ -80,8 +82,8 @@ mod tests {
|
|||||||
|
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
if let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command {
|
if let Some(Command::Radarr(RadarrCommand::List(credits_command))) = result.unwrap().command {
|
||||||
assert_eq!(refresh_command, expected_args);
|
assert_eq!(credits_command, expected_args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +123,7 @@ mod tests {
|
|||||||
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
|
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
|
||||||
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
|
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
|
||||||
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
|
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
|
||||||
|
#[case(RadarrListCommand::DiskSpace, RadarrEvent::GetDiskSpace)]
|
||||||
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
|
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
|
||||||
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
|
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
|
||||||
#[case(RadarrListCommand::QualityProfiles, RadarrEvent::GetQualityProfiles)]
|
#[case(RadarrListCommand::QualityProfiles, RadarrEvent::GetQualityProfiles)]
|
||||||
@@ -130,7 +133,7 @@ mod tests {
|
|||||||
#[case(RadarrListCommand::Tasks, RadarrEvent::GetTasks)]
|
#[case(RadarrListCommand::Tasks, RadarrEvent::GetTasks)]
|
||||||
#[case(RadarrListCommand::Updates, RadarrEvent::GetUpdates)]
|
#[case(RadarrListCommand::Updates, RadarrEvent::GetUpdates)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_handle_list_blocklist_command(
|
async fn test_handle_list_command(
|
||||||
#[case] list_command: RadarrListCommand,
|
#[case] list_command: RadarrListCommand,
|
||||||
#[case] expected_radarr_event: RadarrEvent,
|
#[case] expected_radarr_event: RadarrEvent,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+50
-18
@@ -12,8 +12,7 @@ use tokio::sync::Mutex;
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
|
||||||
use crate::cli::CliCommandHandler;
|
use crate::cli::CliCommandHandler;
|
||||||
use crate::execute_network_event;
|
use crate::models::radarr_models::{RadarrReleaseDownloadBody, RadarrTaskName};
|
||||||
use crate::models::radarr_models::{ReleaseDownloadBody, TaskName};
|
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
use crate::network::NetworkTrait;
|
use crate::network::NetworkTrait;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -86,7 +85,7 @@ pub enum RadarrCommand {
|
|||||||
ManualSearch {
|
ManualSearch {
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
help = "The Radarr ID of the movie whose releases you wish to fetch and list",
|
help = "The Radarr ID of the movie whose releases you wish to fetch",
|
||||||
required = true
|
required = true
|
||||||
)]
|
)]
|
||||||
movie_id: i64,
|
movie_id: i64,
|
||||||
@@ -108,7 +107,7 @@ pub enum RadarrCommand {
|
|||||||
value_enum,
|
value_enum,
|
||||||
required = true
|
required = true
|
||||||
)]
|
)]
|
||||||
task_name: TaskName,
|
task_name: RadarrTaskName,
|
||||||
},
|
},
|
||||||
#[command(
|
#[command(
|
||||||
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
|
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
|
||||||
@@ -117,7 +116,7 @@ pub enum RadarrCommand {
|
|||||||
#[arg(long, help = "The ID of the indexer to test", required = true)]
|
#[arg(long, help = "The ID of the indexer to test", required = true)]
|
||||||
indexer_id: i64,
|
indexer_id: i64,
|
||||||
},
|
},
|
||||||
#[command(about = "Test all indexers")]
|
#[command(about = "Test all Radarr indexers")]
|
||||||
TestAllIndexers,
|
TestAllIndexers,
|
||||||
#[command(about = "Trigger an automatic search for the movie with the specified ID")]
|
#[command(about = "Trigger an automatic search for the movie with the specified ID")]
|
||||||
TriggerAutomaticSearch {
|
TriggerAutomaticSearch {
|
||||||
@@ -155,8 +154,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(self) -> Result<()> {
|
async fn handle(self) -> Result<String> {
|
||||||
match self.command {
|
let result = match self.command {
|
||||||
RadarrCommand::Add(add_command) => {
|
RadarrCommand::Add(add_command) => {
|
||||||
RadarrAddCommandHandler::with(self.app, add_command, self.network)
|
RadarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||||
.handle()
|
.handle()
|
||||||
@@ -192,41 +191,74 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
|
|||||||
.network
|
.network
|
||||||
.handle_network_event(RadarrEvent::GetBlocklist.into())
|
.handle_network_event(RadarrEvent::GetBlocklist.into())
|
||||||
.await?;
|
.await?;
|
||||||
execute_network_event!(self, RadarrEvent::ClearBlocklist);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::ClearBlocklist.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrCommand::DownloadRelease {
|
RadarrCommand::DownloadRelease {
|
||||||
guid,
|
guid,
|
||||||
indexer_id,
|
indexer_id,
|
||||||
movie_id,
|
movie_id,
|
||||||
} => {
|
} => {
|
||||||
let params = ReleaseDownloadBody {
|
let params = RadarrReleaseDownloadBody {
|
||||||
guid,
|
guid,
|
||||||
indexer_id,
|
indexer_id,
|
||||||
movie_id,
|
movie_id,
|
||||||
};
|
};
|
||||||
execute_network_event!(self, RadarrEvent::DownloadRelease(Some(params)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::DownloadRelease(Some(params)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrCommand::ManualSearch { movie_id } => {
|
RadarrCommand::ManualSearch { movie_id } => {
|
||||||
println!("Searching for releases. This may take a minute...");
|
println!("Searching for releases. This may take a minute...");
|
||||||
execute_network_event!(self, RadarrEvent::GetReleases(Some(movie_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::GetReleases(Some(movie_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrCommand::SearchNewMovie { query } => {
|
RadarrCommand::SearchNewMovie { query } => {
|
||||||
execute_network_event!(self, RadarrEvent::SearchNewMovie(Some(query)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::SearchNewMovie(Some(query)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrCommand::StartTask { task_name } => {
|
RadarrCommand::StartTask { task_name } => {
|
||||||
execute_network_event!(self, RadarrEvent::StartTask(Some(task_name)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::StartTask(Some(task_name)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrCommand::TestIndexer { indexer_id } => {
|
RadarrCommand::TestIndexer { indexer_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::TestIndexer(Some(indexer_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::TestIndexer(Some(indexer_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrCommand::TestAllIndexers => {
|
RadarrCommand::TestAllIndexers => {
|
||||||
execute_network_event!(self, RadarrEvent::TestAllIndexers);
|
println!("Testing all Radarr indexers. This may take a minute...");
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::TestAllIndexers.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrCommand::TriggerAutomaticSearch { movie_id } => {
|
RadarrCommand::TriggerAutomaticSearch { movie_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::TriggerAutomaticSearch(Some(movie_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::TriggerAutomaticSearch(Some(movie_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_download_release_requires_movie_id() {
|
fn test_download_release_requires_movie_id() {
|
||||||
let result = Cli::command().try_get_matches_from([
|
let result = Cli::command().try_get_matches_from([
|
||||||
"managarr",
|
"managarr",
|
||||||
@@ -50,7 +50,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_download_release_requires_guid() {
|
fn test_download_release_requires_guid() {
|
||||||
let result = Cli::command().try_get_matches_from([
|
let result = Cli::command().try_get_matches_from([
|
||||||
"managarr",
|
"managarr",
|
||||||
@@ -69,7 +69,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_download_release_requires_indexer_id() {
|
fn test_download_release_requires_indexer_id() {
|
||||||
let result = Cli::command().try_get_matches_from([
|
let result = Cli::command().try_get_matches_from([
|
||||||
"managarr",
|
"managarr",
|
||||||
@@ -105,7 +105,7 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_manual_search_requires_movie_id() {
|
fn test_manual_search_requires_movie_id() {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "manual-search"]);
|
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "manual-search"]);
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_search_new_movie_requires_query() {
|
fn test_search_new_movie_requires_query() {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "search-new-movie"]);
|
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "search-new-movie"]);
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_start_task_requires_task_name() {
|
fn test_start_task_requires_task_name() {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "start-task"]);
|
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "start-task"]);
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_start_task_task_name_validation() {
|
fn test_start_task_task_name_validation() {
|
||||||
let result = Cli::command().try_get_matches_from([
|
let result = Cli::command().try_get_matches_from([
|
||||||
"managarr",
|
"managarr",
|
||||||
@@ -191,7 +191,7 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_test_indexer_requires_indexer_id() {
|
fn test_test_indexer_requires_indexer_id() {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "test-indexer"]);
|
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "test-indexer"]);
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn test_trigger_automatic_search_requires_movie_id() {
|
fn test_trigger_automatic_search_requires_movie_id() {
|
||||||
let result =
|
let result =
|
||||||
Cli::command().try_get_matches_from(["managarr", "radarr", "trigger-automatic-search"]);
|
Cli::command().try_get_matches_from(["managarr", "radarr", "trigger-automatic-search"]);
|
||||||
@@ -261,8 +261,8 @@ mod tests {
|
|||||||
},
|
},
|
||||||
models::{
|
models::{
|
||||||
radarr_models::{
|
radarr_models::{
|
||||||
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrSerdeable, ReleaseDownloadBody,
|
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrReleaseDownloadBody,
|
||||||
TaskName,
|
RadarrSerdeable, RadarrTaskName,
|
||||||
},
|
},
|
||||||
Serdeable,
|
Serdeable,
|
||||||
},
|
},
|
||||||
@@ -304,7 +304,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_download_release_command() {
|
async fn test_download_release_command() {
|
||||||
let expected_release_download_body = ReleaseDownloadBody {
|
let expected_release_download_body = RadarrReleaseDownloadBody {
|
||||||
guid: "guid".to_owned(),
|
guid: "guid".to_owned(),
|
||||||
indexer_id: 1,
|
indexer_id: 1,
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
@@ -389,7 +389,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_start_task_command() {
|
async fn test_start_task_command() {
|
||||||
let expected_task_name = TaskName::ApplicationCheckUpdate;
|
let expected_task_name = RadarrTaskName::ApplicationCheckUpdate;
|
||||||
let mut mock_network = MockNetworkTrait::new();
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
mock_network
|
mock_network
|
||||||
.expect_handle_network_event()
|
.expect_handle_network_event()
|
||||||
@@ -404,7 +404,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
let start_task_command = RadarrCommand::StartTask {
|
let start_task_command = RadarrCommand::StartTask {
|
||||||
task_name: TaskName::ApplicationCheckUpdate,
|
task_name: RadarrTaskName::ApplicationCheckUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = RadarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
|
let result = RadarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{CliCommandHandler, Command},
|
cli::{CliCommandHandler, Command},
|
||||||
execute_network_event,
|
|
||||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ mod refresh_command_handler_tests;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
pub enum RadarrRefreshCommand {
|
pub enum RadarrRefreshCommand {
|
||||||
#[command(about = "Refresh all movie data for all movies in your library")]
|
#[command(about = "Refresh all movie data for all movies in your Radarr library")]
|
||||||
AllMovies,
|
AllMovies,
|
||||||
#[command(about = "Refresh movie data and scan disk for the movie with the given ID")]
|
#[command(about = "Refresh movie data and scan disk for the movie with the given ID")]
|
||||||
Movie {
|
Movie {
|
||||||
@@ -63,22 +62,38 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(self) -> Result<()> {
|
async fn handle(self) -> Result<String> {
|
||||||
match self.command {
|
let result = match self.command {
|
||||||
RadarrRefreshCommand::AllMovies => {
|
RadarrRefreshCommand::AllMovies => {
|
||||||
execute_network_event!(self, RadarrEvent::UpdateAllMovies);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::UpdateAllMovies.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrRefreshCommand::Collections => {
|
RadarrRefreshCommand::Collections => {
|
||||||
execute_network_event!(self, RadarrEvent::UpdateCollections);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::UpdateCollections.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrRefreshCommand::Downloads => {
|
RadarrRefreshCommand::Downloads => {
|
||||||
execute_network_event!(self, RadarrEvent::UpdateDownloads);
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::UpdateDownloads.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
RadarrRefreshCommand::Movie { movie_id } => {
|
RadarrRefreshCommand::Movie { movie_id } => {
|
||||||
execute_network_event!(self, RadarrEvent::UpdateAndScan(Some(movie_id)));
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(RadarrEvent::UpdateAndScan(Some(movie_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod tests {
|
|||||||
use crate::cli::radarr::RadarrCommand;
|
use crate::cli::radarr::RadarrCommand;
|
||||||
use crate::cli::Command;
|
use crate::cli::Command;
|
||||||
use crate::Cli;
|
use crate::Cli;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_refresh_command_from() {
|
fn test_radarr_refresh_command_from() {
|
||||||
@@ -81,7 +82,7 @@ mod tests {
|
|||||||
#[case(RadarrRefreshCommand::Collections, RadarrEvent::UpdateCollections)]
|
#[case(RadarrRefreshCommand::Collections, RadarrEvent::UpdateCollections)]
|
||||||
#[case(RadarrRefreshCommand::Downloads, RadarrEvent::UpdateDownloads)]
|
#[case(RadarrRefreshCommand::Downloads, RadarrEvent::UpdateDownloads)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_handle_list_blocklist_command(
|
async fn test_handle_refresh_command(
|
||||||
#[case] refresh_command: RadarrRefreshCommand,
|
#[case] refresh_command: RadarrRefreshCommand,
|
||||||
#[case] expected_radarr_event: RadarrEvent,
|
#[case] expected_radarr_event: RadarrEvent,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{ArgAction, Subcommand};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
models::sonarr_models::{AddSeriesBody, AddSeriesOptions, SeriesMonitor, SeriesType},
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "add_command_handler_tests.rs"]
|
||||||
|
mod add_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrAddCommand {
|
||||||
|
#[command(about = "Add a new series to your Sonarr library")]
|
||||||
|
Series {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The TVDB ID of the series you wish to add to your library",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
tvdb_id: i64,
|
||||||
|
#[arg(long, help = "The title of the series", required = true)]
|
||||||
|
title: String,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The root folder path where all series data and metadata should live",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
root_folder_path: String,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The ID of the quality profile to use for this series",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
quality_profile_id: i64,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The ID of the language profile to use for this series",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
language_profile_id: i64,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The type of series",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = SeriesType::default()
|
||||||
|
)]
|
||||||
|
series_type: SeriesType,
|
||||||
|
#[arg(long, help = "Disable monitoring for this series")]
|
||||||
|
disable_monitoring: bool,
|
||||||
|
#[arg(long, help = "Don't use season folders for this series")]
|
||||||
|
disable_season_folders: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Tag IDs to tag the series with",
|
||||||
|
value_parser,
|
||||||
|
action = ArgAction::Append
|
||||||
|
)]
|
||||||
|
tag: Vec<i64>,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "What Sonarr should monitor",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = SeriesMonitor::default()
|
||||||
|
)]
|
||||||
|
monitor: SeriesMonitor,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Tell Sonarr to not start a search for this series once it's added to your library"
|
||||||
|
)]
|
||||||
|
no_search_for_series: 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<SonarrAddCommand> for Command {
|
||||||
|
fn from(value: SonarrAddCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::Add(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrAddCommandHandler<'a, 'b> {
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrAddCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHandler<'a, 'b> {
|
||||||
|
fn with(
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrAddCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrAddCommandHandler {
|
||||||
|
_app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrAddCommand::Series {
|
||||||
|
tvdb_id,
|
||||||
|
title,
|
||||||
|
root_folder_path,
|
||||||
|
quality_profile_id,
|
||||||
|
language_profile_id,
|
||||||
|
series_type,
|
||||||
|
disable_monitoring,
|
||||||
|
disable_season_folders,
|
||||||
|
tag: tags,
|
||||||
|
monitor,
|
||||||
|
no_search_for_series,
|
||||||
|
} => {
|
||||||
|
let body = AddSeriesBody {
|
||||||
|
tvdb_id,
|
||||||
|
title,
|
||||||
|
monitored: !disable_monitoring,
|
||||||
|
root_folder_path,
|
||||||
|
quality_profile_id,
|
||||||
|
language_profile_id,
|
||||||
|
series_type: series_type.to_string(),
|
||||||
|
season_folder: !disable_season_folders,
|
||||||
|
tags,
|
||||||
|
add_options: AddSeriesOptions {
|
||||||
|
monitor: monitor.to_string(),
|
||||||
|
search_for_cutoff_unmet_episodes: !no_search_for_series,
|
||||||
|
search_for_missing_episodes: !no_search_for_series,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::AddSeries(Some(body)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrAddCommand::RootFolder { root_folder_path } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::AddRootFolder(Some(root_folder_path)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrAddCommand::Tag { name } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::AddTag(name).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,582 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
sonarr::{add_command_handler::SonarrAddCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
},
|
||||||
|
Cli,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_add_command_from() {
|
||||||
|
let command = SonarrAddCommand::Tag {
|
||||||
|
name: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Sonarr(SonarrCommand::Add(command)));
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use crate::models::sonarr_models::{SeriesMonitor, SeriesType};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_root_folder_requires_arguments() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "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 = SonarrAddCommand::RootFolder {
|
||||||
|
root_folder_path: "/nfs/test".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"root-folder",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/nfs/test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(add_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_requires_arguments() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "series"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_requires_tvdb_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--root-folder-path",
|
||||||
|
"test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_requires_title() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
"--root-folder-path",
|
||||||
|
"test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_requires_root_folder_path() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_requires_quality_profile_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
"--root-folder-path",
|
||||||
|
"test",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_requires_language_profile_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
"--root-folder-path",
|
||||||
|
"test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_add_series_assert_argument_flags_require_args(
|
||||||
|
#[values("--series-type", "--tag", "--monitor")] flag: &str,
|
||||||
|
) {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
flag,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_all_arguments_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_series_type_validation() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
"--series-type",
|
||||||
|
"test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_monitor_validation() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--tvdb-id",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
"1",
|
||||||
|
"--monitor",
|
||||||
|
"test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_defaults() {
|
||||||
|
let expected_args = SonarrAddCommand::Series {
|
||||||
|
tvdb_id: 1,
|
||||||
|
title: "test".to_owned(),
|
||||||
|
root_folder_path: "/test".to_owned(),
|
||||||
|
quality_profile_id: 1,
|
||||||
|
language_profile_id: 1,
|
||||||
|
series_type: SeriesType::default(),
|
||||||
|
disable_monitoring: false,
|
||||||
|
disable_season_folders: false,
|
||||||
|
tag: vec![],
|
||||||
|
monitor: SeriesMonitor::default(),
|
||||||
|
no_search_for_series: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(add_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_tags_is_repeatable() {
|
||||||
|
let expected_args = SonarrAddCommand::Series {
|
||||||
|
tvdb_id: 1,
|
||||||
|
title: "test".to_owned(),
|
||||||
|
root_folder_path: "/test".to_owned(),
|
||||||
|
quality_profile_id: 1,
|
||||||
|
language_profile_id: 1,
|
||||||
|
series_type: SeriesType::default(),
|
||||||
|
disable_monitoring: false,
|
||||||
|
disable_season_folders: false,
|
||||||
|
tag: vec![1, 2],
|
||||||
|
monitor: SeriesMonitor::default(),
|
||||||
|
no_search_for_series: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
"--tag",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"2",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(add_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_all_args_defined() {
|
||||||
|
let expected_args = SonarrAddCommand::Series {
|
||||||
|
tvdb_id: 1,
|
||||||
|
title: "test".to_owned(),
|
||||||
|
root_folder_path: "/test".to_owned(),
|
||||||
|
quality_profile_id: 1,
|
||||||
|
language_profile_id: 1,
|
||||||
|
series_type: SeriesType::Anime,
|
||||||
|
disable_monitoring: true,
|
||||||
|
disable_season_folders: true,
|
||||||
|
tag: vec![1, 2],
|
||||||
|
monitor: SeriesMonitor::Future,
|
||||||
|
no_search_for_series: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"add",
|
||||||
|
"series",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/test",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--series-type",
|
||||||
|
"anime",
|
||||||
|
"--disable-monitoring",
|
||||||
|
"--disable-season-folders",
|
||||||
|
"--tvdb-id",
|
||||||
|
"1",
|
||||||
|
"--title",
|
||||||
|
"test",
|
||||||
|
"--tag",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"2",
|
||||||
|
"--monitor",
|
||||||
|
"future",
|
||||||
|
"--no-search-for-series",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::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", "sonarr", "add", "tag"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_tag_success() {
|
||||||
|
let expected_args = SonarrAddCommand::Tag {
|
||||||
|
name: "test".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from(["managarr", "sonarr", "add", "tag", "--name", "test"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(add_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod handler {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{sonarr::add_command_handler::SonarrAddCommandHandler, CliCommandHandler},
|
||||||
|
models::{
|
||||||
|
sonarr_models::{
|
||||||
|
AddSeriesBody, AddSeriesOptions, SeriesMonitor, SeriesType, SonarrSerdeable,
|
||||||
|
},
|
||||||
|
Serdeable,
|
||||||
|
},
|
||||||
|
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
#[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>(
|
||||||
|
SonarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let add_root_folder_command = SonarrAddCommand::RootFolder {
|
||||||
|
root_folder_path: expected_root_folder_path,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_add_series_command() {
|
||||||
|
let expected_add_series_body = AddSeriesBody {
|
||||||
|
tvdb_id: 1,
|
||||||
|
title: "test".to_owned(),
|
||||||
|
root_folder_path: "/test".to_owned(),
|
||||||
|
quality_profile_id: 1,
|
||||||
|
language_profile_id: 1,
|
||||||
|
series_type: "anime".to_owned(),
|
||||||
|
monitored: false,
|
||||||
|
tags: vec![1, 2],
|
||||||
|
season_folder: false,
|
||||||
|
add_options: AddSeriesOptions {
|
||||||
|
monitor: "future".to_owned(),
|
||||||
|
search_for_cutoff_unmet_episodes: false,
|
||||||
|
search_for_missing_episodes: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::AddSeries(Some(expected_add_series_body)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let add_series_command = SonarrAddCommand::Series {
|
||||||
|
tvdb_id: 1,
|
||||||
|
title: "test".to_owned(),
|
||||||
|
root_folder_path: "/test".to_owned(),
|
||||||
|
quality_profile_id: 1,
|
||||||
|
language_profile_id: 1,
|
||||||
|
series_type: SeriesType::Anime,
|
||||||
|
disable_monitoring: true,
|
||||||
|
disable_season_folders: true,
|
||||||
|
tag: vec![1, 2],
|
||||||
|
monitor: SeriesMonitor::Future,
|
||||||
|
no_search_for_series: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrAddCommandHandler::with(&app_arc, add_series_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>(
|
||||||
|
SonarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let add_tag_command = SonarrAddCommand::Tag {
|
||||||
|
name: expected_tag_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
models::sonarr_models::DeleteSeriesParams,
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "delete_command_handler_tests.rs"]
|
||||||
|
mod delete_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrDeleteCommand {
|
||||||
|
#[command(about = "Delete the specified item from the Sonarr 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 specified episode file from disk")]
|
||||||
|
EpisodeFile {
|
||||||
|
#[arg(long, help = "The ID of the episode file to delete", required = true)]
|
||||||
|
episode_file_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 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 a series from your Sonarr library")]
|
||||||
|
Series {
|
||||||
|
#[arg(long, help = "The ID of the series to delete", required = true)]
|
||||||
|
series_id: i64,
|
||||||
|
#[arg(long, help = "Delete the series files from disk as well")]
|
||||||
|
delete_files_from_disk: bool,
|
||||||
|
#[arg(long, help = "Add a list exclusion for this series")]
|
||||||
|
add_list_exclusion: bool,
|
||||||
|
},
|
||||||
|
#[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<SonarrDeleteCommand> for Command {
|
||||||
|
fn from(value: SonarrDeleteCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::Delete(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrDeleteCommandHandler<'a, 'b> {
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrDeleteCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteCommandHandler<'a, 'b> {
|
||||||
|
fn with(
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrDeleteCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrDeleteCommandHandler {
|
||||||
|
_app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let resp = match self.command {
|
||||||
|
SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrDeleteCommand::Download { download_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DeleteDownload(Some(download_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrDeleteCommand::EpisodeFile { episode_file_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DeleteEpisodeFile(Some(episode_file_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrDeleteCommand::Indexer { indexer_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DeleteIndexer(Some(indexer_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrDeleteCommand::RootFolder { root_folder_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DeleteRootFolder(Some(root_folder_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrDeleteCommand::Series {
|
||||||
|
series_id,
|
||||||
|
delete_files_from_disk,
|
||||||
|
add_list_exclusion,
|
||||||
|
} => {
|
||||||
|
let delete_series_params = DeleteSeriesParams {
|
||||||
|
id: series_id,
|
||||||
|
delete_series_files: delete_files_from_disk,
|
||||||
|
add_list_exclusion,
|
||||||
|
};
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DeleteSeries(Some(delete_series_params)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrDeleteCommand::Tag { tag_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DeleteTag(tag_id).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,466 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
sonarr::{delete_command_handler::SonarrDeleteCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
},
|
||||||
|
Cli,
|
||||||
|
};
|
||||||
|
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_delete_command_from() {
|
||||||
|
let command = SonarrDeleteCommand::BlocklistItem {
|
||||||
|
blocklist_item_id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Sonarr(SonarrCommand::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", "sonarr", "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 = SonarrDeleteCommand::BlocklistItem {
|
||||||
|
blocklist_item_id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"delete",
|
||||||
|
"blocklist-item",
|
||||||
|
"--blocklist-item-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::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", "sonarr", "delete", "download"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_download_success() {
|
||||||
|
let expected_args = SonarrDeleteCommand::Download { download_id: 1 };
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"delete",
|
||||||
|
"download",
|
||||||
|
"--download-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||||
|
{
|
||||||
|
assert_eq!(delete_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_episode_file_requires_arguments() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "episode-file"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_episode_file_success() {
|
||||||
|
let expected_args = SonarrDeleteCommand::EpisodeFile { episode_file_id: 1 };
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"delete",
|
||||||
|
"episode-file",
|
||||||
|
"--episode-file-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::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", "sonarr", "delete", "indexer"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_indexer_success() {
|
||||||
|
let expected_args = SonarrDeleteCommand::Indexer { indexer_id: 1 };
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"delete",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::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", "sonarr", "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 = SonarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"delete",
|
||||||
|
"root-folder",
|
||||||
|
"--root-folder-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||||
|
{
|
||||||
|
assert_eq!(delete_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_series_requires_arguments() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "series"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_series_defaults() {
|
||||||
|
let expected_args = SonarrDeleteCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
delete_files_from_disk: false,
|
||||||
|
add_list_exclusion: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
Cli::try_parse_from(["managarr", "sonarr", "delete", "series", "--series-id", "1"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||||
|
{
|
||||||
|
assert_eq!(delete_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_series_all_args_defined() {
|
||||||
|
let expected_args = SonarrDeleteCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
delete_files_from_disk: true,
|
||||||
|
add_list_exclusion: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"delete",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--delete-files-from-disk",
|
||||||
|
"--add-list-exclusion",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::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", "sonarr", "delete", "tag"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_tag_success() {
|
||||||
|
let expected_args = SonarrDeleteCommand::Tag { tag_id: 1 };
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from(["managarr", "sonarr", "delete", "tag", "--tag-id", "1"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::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::{
|
||||||
|
sonarr::delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler},
|
||||||
|
CliCommandHandler,
|
||||||
|
},
|
||||||
|
models::{
|
||||||
|
sonarr_models::{DeleteSeriesParams, SonarrSerdeable},
|
||||||
|
Serdeable,
|
||||||
|
},
|
||||||
|
network::{sonarr_network::SonarrEvent, 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>(
|
||||||
|
SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let delete_blocklist_item_command = SonarrDeleteCommand::BlocklistItem {
|
||||||
|
blocklist_item_id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrDeleteCommandHandler::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>(
|
||||||
|
SonarrEvent::DeleteDownload(Some(expected_download_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let delete_download_command = SonarrDeleteCommand::Download { download_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrDeleteCommandHandler::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>(
|
||||||
|
SonarrEvent::DeleteIndexer(Some(expected_indexer_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let delete_indexer_command = SonarrDeleteCommand::Indexer { indexer_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrDeleteCommandHandler::with(&app_arc, delete_indexer_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>(
|
||||||
|
SonarrEvent::DeleteRootFolder(Some(expected_root_folder_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let delete_root_folder_command = SonarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_delete_series_command() {
|
||||||
|
let expected_delete_series_params = DeleteSeriesParams {
|
||||||
|
id: 1,
|
||||||
|
delete_series_files: true,
|
||||||
|
add_list_exclusion: true,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::DeleteSeries(Some(expected_delete_series_params)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let delete_series_command = SonarrDeleteCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
delete_files_from_disk: true,
|
||||||
|
add_list_exclusion: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrDeleteCommandHandler::with(&app_arc, delete_series_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>(
|
||||||
|
SonarrEvent::DeleteTag(expected_tag_id).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let delete_tag_command = SonarrDeleteCommand::Tag { tag_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
models::sonarr_models::SonarrReleaseDownloadBody,
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "download_command_handler_tests.rs"]
|
||||||
|
mod download_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrDownloadCommand {
|
||||||
|
#[command(about = "Manually download the given series release for the specified series ID")]
|
||||||
|
Series {
|
||||||
|
#[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 series ID that the release is associated with",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
},
|
||||||
|
#[command(
|
||||||
|
about = "Manually download the given season release corresponding to the series specified with the series ID"
|
||||||
|
)]
|
||||||
|
Season {
|
||||||
|
#[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 series ID that the release is associated with",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The season number that the release corresponds to",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
season_number: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "Manually download the given episode release for the specified episode ID")]
|
||||||
|
Episode {
|
||||||
|
#[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 episode ID that the release is associated with",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
episode_id: i64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrDownloadCommand> for Command {
|
||||||
|
fn from(value: SonarrDownloadCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::Download(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrDownloadCommandHandler<'a, 'b> {
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrDownloadCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDownloadCommand>
|
||||||
|
for SonarrDownloadCommandHandler<'a, 'b>
|
||||||
|
{
|
||||||
|
fn with(
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrDownloadCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrDownloadCommandHandler {
|
||||||
|
_app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrDownloadCommand::Series {
|
||||||
|
guid,
|
||||||
|
indexer_id,
|
||||||
|
series_id,
|
||||||
|
} => {
|
||||||
|
let params = SonarrReleaseDownloadBody {
|
||||||
|
guid,
|
||||||
|
indexer_id,
|
||||||
|
series_id: Some(series_id),
|
||||||
|
..SonarrReleaseDownloadBody::default()
|
||||||
|
};
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrDownloadCommand::Season {
|
||||||
|
guid,
|
||||||
|
indexer_id,
|
||||||
|
series_id,
|
||||||
|
season_number,
|
||||||
|
} => {
|
||||||
|
let params = SonarrReleaseDownloadBody {
|
||||||
|
guid,
|
||||||
|
indexer_id,
|
||||||
|
series_id: Some(series_id),
|
||||||
|
season_number: Some(season_number),
|
||||||
|
..SonarrReleaseDownloadBody::default()
|
||||||
|
};
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrDownloadCommand::Episode {
|
||||||
|
guid,
|
||||||
|
indexer_id,
|
||||||
|
episode_id,
|
||||||
|
} => {
|
||||||
|
let params = SonarrReleaseDownloadBody {
|
||||||
|
guid,
|
||||||
|
indexer_id,
|
||||||
|
episode_id: Some(episode_id),
|
||||||
|
..SonarrReleaseDownloadBody::default()
|
||||||
|
};
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
sonarr::{download_command_handler::SonarrDownloadCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
},
|
||||||
|
Cli,
|
||||||
|
};
|
||||||
|
use clap::CommandFactory;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_download_command_from() {
|
||||||
|
let command = SonarrDownloadCommand::Series {
|
||||||
|
guid: "Test".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
series_id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Sonarr(SonarrCommand::Download(command)));
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use super::*;
|
||||||
|
use clap::error::ErrorKind;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_series_requires_series_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"series",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_series_requires_guid() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"series",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_series_requires_indexer_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"series",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_series_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"series",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_season_requires_series_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"season",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--season-number",
|
||||||
|
"1",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_season_requires_season_number() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"season",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_season_requires_guid() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"season",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--season-number",
|
||||||
|
"1",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_season_requires_indexer_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"season",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
"--season-number",
|
||||||
|
"1",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_season_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"season",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--season-number",
|
||||||
|
"1",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_episode_requires_episode_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"episode",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_episode_requires_guid() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"episode",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--episode-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_episode_requires_indexer_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"episode",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
"--episode-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_episode_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"download",
|
||||||
|
"episode",
|
||||||
|
"--guid",
|
||||||
|
"1",
|
||||||
|
"--episode-id",
|
||||||
|
"1",
|
||||||
|
"--indexer-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::{
|
||||||
|
sonarr::download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler},
|
||||||
|
CliCommandHandler,
|
||||||
|
},
|
||||||
|
models::{
|
||||||
|
sonarr_models::{SonarrReleaseDownloadBody, SonarrSerdeable},
|
||||||
|
Serdeable,
|
||||||
|
},
|
||||||
|
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_download_series_release_command() {
|
||||||
|
let expected_release_download_body = SonarrReleaseDownloadBody {
|
||||||
|
guid: "guid".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
series_id: Some(1),
|
||||||
|
..SonarrReleaseDownloadBody::default()
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let download_release_command = SonarrDownloadCommand::Series {
|
||||||
|
guid: "guid".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
series_id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_download_season_release_command() {
|
||||||
|
let expected_release_download_body = SonarrReleaseDownloadBody {
|
||||||
|
guid: "guid".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
series_id: Some(1),
|
||||||
|
season_number: Some(1),
|
||||||
|
..SonarrReleaseDownloadBody::default()
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let download_release_command = SonarrDownloadCommand::Season {
|
||||||
|
guid: "guid".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
series_id: 1,
|
||||||
|
season_number: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_download_episode_release_command() {
|
||||||
|
let expected_release_download_body = SonarrReleaseDownloadBody {
|
||||||
|
guid: "guid".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
episode_id: Some(1),
|
||||||
|
..SonarrReleaseDownloadBody::default()
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let download_release_command = SonarrDownloadCommand::Episode {
|
||||||
|
guid: "guid".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
episode_id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{ArgAction, ArgGroup, Subcommand};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{mutex_flags_or_option, CliCommandHandler, Command},
|
||||||
|
models::{
|
||||||
|
servarr_models::EditIndexerParams,
|
||||||
|
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
|
||||||
|
Serdeable,
|
||||||
|
},
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "edit_command_handler_tests.rs"]
|
||||||
|
mod edit_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrEditCommand {
|
||||||
|
#[command(
|
||||||
|
about = "Edit and indexer settings that apply to all indexers",
|
||||||
|
group(
|
||||||
|
ArgGroup::new("edit_settings")
|
||||||
|
.args([
|
||||||
|
"maximum_size",
|
||||||
|
"minimum_age",
|
||||||
|
"retention",
|
||||||
|
"rss_sync_interval",
|
||||||
|
]).required(true)
|
||||||
|
.multiple(true))
|
||||||
|
)]
|
||||||
|
AllIndexerSettings {
|
||||||
|
#[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 = "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>,
|
||||||
|
},
|
||||||
|
#[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 Sonarr that this indexer should be used when Sonarr periodically looks for releases via RSS Sync",
|
||||||
|
conflicts_with = "disable_rss"
|
||||||
|
)]
|
||||||
|
enable_rss: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Disable using this indexer when Sonarr periodically looks for releases via RSS Sync",
|
||||||
|
conflicts_with = "enable_rss"
|
||||||
|
)]
|
||||||
|
disable_rss: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Indicate to Sonarr that this indexer should be used when automatic searches are performed via the UI or by Sonarr",
|
||||||
|
conflicts_with = "disable_automatic_search"
|
||||||
|
)]
|
||||||
|
enable_automatic_search: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Sonarr",
|
||||||
|
conflicts_with = "enable_automatic_search"
|
||||||
|
)]
|
||||||
|
disable_automatic_search: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Indicate to Sonarr that this indexer should be used when an interactive search is used",
|
||||||
|
conflicts_with = "disable_interactive_search"
|
||||||
|
)]
|
||||||
|
enable_interactive_search: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Disable using this indexer whenever an interactive search is performed",
|
||||||
|
conflicts_with = "enable_interactive_search"
|
||||||
|
)]
|
||||||
|
disable_interactive_search: bool,
|
||||||
|
#[arg(long, help = "The URL of the indexer")]
|
||||||
|
url: Option<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 series with at least one matching tag ID. Leave blank to use with all series.",
|
||||||
|
value_parser,
|
||||||
|
action = ArgAction::Append,
|
||||||
|
conflicts_with = "clear_tags"
|
||||||
|
)]
|
||||||
|
tag: Option<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, Sonarr 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 series",
|
||||||
|
group(
|
||||||
|
ArgGroup::new("edit_series")
|
||||||
|
.args([
|
||||||
|
"enable_monitoring",
|
||||||
|
"disable_monitoring",
|
||||||
|
"enable_season_folders",
|
||||||
|
"disable_season_folders",
|
||||||
|
"series_type",
|
||||||
|
"quality_profile_id",
|
||||||
|
"language_profile_id",
|
||||||
|
"root_folder_path",
|
||||||
|
"tag",
|
||||||
|
"clear_tags"
|
||||||
|
]).required(true)
|
||||||
|
.multiple(true))
|
||||||
|
)]
|
||||||
|
Series {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The ID of the series whose settings you want to edit",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Enable monitoring of this series in Sonarr so Sonarr will automatically download this series if it is available",
|
||||||
|
conflicts_with = "disable_monitoring"
|
||||||
|
)]
|
||||||
|
enable_monitoring: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Disable monitoring of this series so Sonarr does not automatically download the series 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
|
||||||
|
)]
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Enable sorting episodes of this series into season folders",
|
||||||
|
conflicts_with = "disable_season_folders"
|
||||||
|
)]
|
||||||
|
enable_season_folders: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Disable sorting episodes of this series into season folders",
|
||||||
|
conflicts_with = "enable_season_folders"
|
||||||
|
)]
|
||||||
|
disable_season_folders: bool,
|
||||||
|
#[arg(long, help = "The type of series", value_enum)]
|
||||||
|
series_type: Option<SeriesType>,
|
||||||
|
#[arg(long, help = "The ID of the quality profile to use for this series")]
|
||||||
|
quality_profile_id: Option<i64>,
|
||||||
|
#[arg(long, help = "The ID of the language profile to use for this series")]
|
||||||
|
language_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 series with",
|
||||||
|
value_parser,
|
||||||
|
action = ArgAction::Append,
|
||||||
|
conflicts_with = "clear_tags"
|
||||||
|
)]
|
||||||
|
tag: Option<Vec<i64>>,
|
||||||
|
#[arg(long, help = "Clear all tags on this series", conflicts_with = "tag")]
|
||||||
|
clear_tags: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrEditCommand> for Command {
|
||||||
|
fn from(value: SonarrEditCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::Edit(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrEditCommandHandler<'a, 'b> {
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrEditCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandHandler<'a, 'b> {
|
||||||
|
fn with(
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrEditCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrEditCommandHandler {
|
||||||
|
_app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrEditCommand::AllIndexerSettings {
|
||||||
|
maximum_size,
|
||||||
|
minimum_age,
|
||||||
|
retention,
|
||||||
|
rss_sync_interval,
|
||||||
|
} => {
|
||||||
|
if let Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(previous_indexer_settings)) = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetAllIndexerSettings.into())
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
let params = IndexerSettings {
|
||||||
|
id: 1,
|
||||||
|
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
|
||||||
|
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
|
||||||
|
retention: retention.unwrap_or(previous_indexer_settings.retention),
|
||||||
|
rss_sync_interval: rss_sync_interval
|
||||||
|
.unwrap_or(previous_indexer_settings.rss_sync_interval),
|
||||||
|
};
|
||||||
|
self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::EditAllIndexerSettings(Some(params)).into())
|
||||||
|
.await?;
|
||||||
|
"All indexer settings updated".to_owned()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SonarrEditCommand::Indexer {
|
||||||
|
indexer_id,
|
||||||
|
name,
|
||||||
|
enable_rss,
|
||||||
|
disable_rss,
|
||||||
|
enable_automatic_search,
|
||||||
|
disable_automatic_search,
|
||||||
|
enable_interactive_search,
|
||||||
|
disable_interactive_search,
|
||||||
|
url,
|
||||||
|
api_key,
|
||||||
|
seed_ratio,
|
||||||
|
tag,
|
||||||
|
priority,
|
||||||
|
clear_tags,
|
||||||
|
} => {
|
||||||
|
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
|
||||||
|
let automatic_search_value =
|
||||||
|
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
|
||||||
|
let interactive_search_value =
|
||||||
|
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
|
||||||
|
let edit_indexer_params = EditIndexerParams {
|
||||||
|
indexer_id,
|
||||||
|
name,
|
||||||
|
enable_rss: rss_value,
|
||||||
|
enable_automatic_search: automatic_search_value,
|
||||||
|
enable_interactive_search: interactive_search_value,
|
||||||
|
url,
|
||||||
|
api_key,
|
||||||
|
seed_ratio,
|
||||||
|
tags: tag,
|
||||||
|
priority,
|
||||||
|
clear_tags,
|
||||||
|
};
|
||||||
|
|
||||||
|
self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)).into())
|
||||||
|
.await?;
|
||||||
|
"Indexer updated".to_owned()
|
||||||
|
}
|
||||||
|
SonarrEditCommand::Series {
|
||||||
|
series_id,
|
||||||
|
enable_monitoring,
|
||||||
|
disable_monitoring,
|
||||||
|
enable_season_folders,
|
||||||
|
disable_season_folders,
|
||||||
|
series_type,
|
||||||
|
quality_profile_id,
|
||||||
|
language_profile_id,
|
||||||
|
root_folder_path,
|
||||||
|
tag,
|
||||||
|
clear_tags,
|
||||||
|
} => {
|
||||||
|
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
|
||||||
|
let season_folders_value =
|
||||||
|
mutex_flags_or_option(enable_season_folders, disable_season_folders);
|
||||||
|
let edit_series_params = EditSeriesParams {
|
||||||
|
series_id,
|
||||||
|
monitored: monitored_value,
|
||||||
|
use_season_folders: season_folders_value,
|
||||||
|
series_type,
|
||||||
|
quality_profile_id,
|
||||||
|
language_profile_id,
|
||||||
|
root_folder_path,
|
||||||
|
tags: tag,
|
||||||
|
clear_tags,
|
||||||
|
};
|
||||||
|
|
||||||
|
self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::EditSeries(Some(edit_series_params)).into())
|
||||||
|
.await?;
|
||||||
|
"Series Updated".to_owned()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,874 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cli::{
|
||||||
|
sonarr::{edit_command_handler::SonarrEditCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_edit_command_from() {
|
||||||
|
let command = SonarrEditCommand::AllIndexerSettings {
|
||||||
|
maximum_size: None,
|
||||||
|
minimum_age: None,
|
||||||
|
retention: None,
|
||||||
|
rss_sync_interval: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Sonarr(SonarrCommand::Edit(command)));
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use crate::{models::sonarr_models::SeriesType, Cli};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_all_indexer_settings_requires_arguments() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "all-indexer-settings"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_edit_all_indexer_settings_assert_argument_flags_require_args(
|
||||||
|
#[values(
|
||||||
|
"--maximum-size",
|
||||||
|
"--minimum-age",
|
||||||
|
"--retention",
|
||||||
|
"--rss-sync-interval"
|
||||||
|
)]
|
||||||
|
flag: &str,
|
||||||
|
) {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"all-indexer-settings",
|
||||||
|
flag,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() {
|
||||||
|
let expected_args = SonarrEditCommand::AllIndexerSettings {
|
||||||
|
maximum_size: Some(1),
|
||||||
|
minimum_age: None,
|
||||||
|
retention: None,
|
||||||
|
rss_sync_interval: None,
|
||||||
|
};
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"all-indexer-settings",
|
||||||
|
"--maximum-size",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(edit_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_all_indexer_settings_all_arguments_defined() {
|
||||||
|
let expected_args = SonarrEditCommand::AllIndexerSettings {
|
||||||
|
maximum_size: Some(1),
|
||||||
|
minimum_age: Some(1),
|
||||||
|
retention: Some(1),
|
||||||
|
rss_sync_interval: Some(1),
|
||||||
|
};
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"all-indexer-settings",
|
||||||
|
"--maximum-size",
|
||||||
|
"1",
|
||||||
|
"--minimum-age",
|
||||||
|
"1",
|
||||||
|
"--retention",
|
||||||
|
"1",
|
||||||
|
"--rss-sync-interval",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(edit_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_requires_arguments() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "indexer"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_with_indexer_id_still_requires_arguments() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_rss_flags_conflict() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--enable-rss",
|
||||||
|
"--disable-rss",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_automatic_search_flags_conflict() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--enable-automatic-search",
|
||||||
|
"--disable-automatic-search",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_interactive_search_flags_conflict() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--enable-interactive-search",
|
||||||
|
"--disable-interactive-search",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_tag_flags_conflict() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"1",
|
||||||
|
"--clear-tags",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_edit_indexer_assert_argument_flags_require_args(
|
||||||
|
#[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str,
|
||||||
|
) {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
flag,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() {
|
||||||
|
let expected_args = SonarrEditCommand::Indexer {
|
||||||
|
indexer_id: 1,
|
||||||
|
name: Some("Test".to_owned()),
|
||||||
|
enable_rss: false,
|
||||||
|
disable_rss: false,
|
||||||
|
enable_automatic_search: false,
|
||||||
|
disable_automatic_search: false,
|
||||||
|
enable_interactive_search: false,
|
||||||
|
disable_interactive_search: false,
|
||||||
|
url: None,
|
||||||
|
api_key: None,
|
||||||
|
seed_ratio: None,
|
||||||
|
tag: None,
|
||||||
|
priority: None,
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--name",
|
||||||
|
"Test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(edit_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_tag_argument_is_repeatable() {
|
||||||
|
let expected_args = SonarrEditCommand::Indexer {
|
||||||
|
indexer_id: 1,
|
||||||
|
name: None,
|
||||||
|
enable_rss: false,
|
||||||
|
disable_rss: false,
|
||||||
|
enable_automatic_search: false,
|
||||||
|
disable_automatic_search: false,
|
||||||
|
enable_interactive_search: false,
|
||||||
|
disable_interactive_search: false,
|
||||||
|
url: None,
|
||||||
|
api_key: None,
|
||||||
|
seed_ratio: None,
|
||||||
|
tag: Some(vec![1, 2]),
|
||||||
|
priority: None,
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"2",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(edit_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_all_arguments_defined() {
|
||||||
|
let expected_args = SonarrEditCommand::Indexer {
|
||||||
|
indexer_id: 1,
|
||||||
|
name: Some("Test".to_owned()),
|
||||||
|
enable_rss: true,
|
||||||
|
disable_rss: false,
|
||||||
|
enable_automatic_search: true,
|
||||||
|
disable_automatic_search: false,
|
||||||
|
enable_interactive_search: true,
|
||||||
|
disable_interactive_search: false,
|
||||||
|
url: Some("http://test.com".to_owned()),
|
||||||
|
api_key: Some("testKey".to_owned()),
|
||||||
|
seed_ratio: Some("1.2".to_owned()),
|
||||||
|
tag: Some(vec![1, 2]),
|
||||||
|
priority: Some(25),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"indexer",
|
||||||
|
"--indexer-id",
|
||||||
|
"1",
|
||||||
|
"--name",
|
||||||
|
"Test",
|
||||||
|
"--enable-rss",
|
||||||
|
"--enable-automatic-search",
|
||||||
|
"--enable-interactive-search",
|
||||||
|
"--url",
|
||||||
|
"http://test.com",
|
||||||
|
"--api-key",
|
||||||
|
"testKey",
|
||||||
|
"--seed-ratio",
|
||||||
|
"1.2",
|
||||||
|
"--tag",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"2",
|
||||||
|
"--priority",
|
||||||
|
"25",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(edit_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_requires_arguments() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "series"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_with_series_id_still_requires_arguments() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_monitoring_flags_conflict() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--enable-monitoring",
|
||||||
|
"--disable-monitoring",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_season_folders_flags_conflict() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--enable-season-folders",
|
||||||
|
"--disable-season-folders",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_tag_flags_conflict() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"1",
|
||||||
|
"--clear-tags",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_edit_series_assert_argument_flags_require_args(
|
||||||
|
#[values(
|
||||||
|
"--series-type",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"--language-profile-id",
|
||||||
|
"--root-folder-path",
|
||||||
|
"--tag"
|
||||||
|
)]
|
||||||
|
flag: &str,
|
||||||
|
) {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
flag,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_series_type_validation() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--series-type",
|
||||||
|
"test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_only_requires_at_least_one_argument_plus_series_id() {
|
||||||
|
let expected_args = SonarrEditCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
enable_monitoring: false,
|
||||||
|
disable_monitoring: false,
|
||||||
|
enable_season_folders: false,
|
||||||
|
disable_season_folders: false,
|
||||||
|
series_type: None,
|
||||||
|
quality_profile_id: None,
|
||||||
|
language_profile_id: None,
|
||||||
|
root_folder_path: Some("/nfs/test".to_owned()),
|
||||||
|
tag: None,
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/nfs/test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(edit_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_tag_argument_is_repeatable() {
|
||||||
|
let expected_args = SonarrEditCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
enable_monitoring: false,
|
||||||
|
disable_monitoring: false,
|
||||||
|
enable_season_folders: false,
|
||||||
|
disable_season_folders: false,
|
||||||
|
series_type: None,
|
||||||
|
quality_profile_id: None,
|
||||||
|
language_profile_id: None,
|
||||||
|
root_folder_path: None,
|
||||||
|
tag: Some(vec![1, 2]),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"2",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(edit_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_series_all_arguments_defined() {
|
||||||
|
let expected_args = SonarrEditCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
enable_monitoring: true,
|
||||||
|
disable_monitoring: false,
|
||||||
|
enable_season_folders: true,
|
||||||
|
disable_season_folders: false,
|
||||||
|
series_type: Some(SeriesType::Anime),
|
||||||
|
quality_profile_id: Some(1),
|
||||||
|
language_profile_id: Some(1),
|
||||||
|
root_folder_path: Some("/nfs/test".to_owned()),
|
||||||
|
tag: Some(vec![1, 2]),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"edit",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--enable-monitoring",
|
||||||
|
"--enable-season-folders",
|
||||||
|
"--series-type",
|
||||||
|
"anime",
|
||||||
|
"--quality-profile-id",
|
||||||
|
"1",
|
||||||
|
"--language-profile-id",
|
||||||
|
"1",
|
||||||
|
"--root-folder-path",
|
||||||
|
"/nfs/test",
|
||||||
|
"--tag",
|
||||||
|
"1",
|
||||||
|
"--tag",
|
||||||
|
"2",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(edit_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod handler {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use serde_json::json;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{
|
||||||
|
sonarr::edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler},
|
||||||
|
CliCommandHandler,
|
||||||
|
},
|
||||||
|
models::{
|
||||||
|
servarr_models::EditIndexerParams,
|
||||||
|
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
|
||||||
|
Serdeable,
|
||||||
|
},
|
||||||
|
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_all_indexer_settings_command() {
|
||||||
|
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||||
|
id: 1,
|
||||||
|
maximum_size: 1,
|
||||||
|
minimum_age: 1,
|
||||||
|
retention: 1,
|
||||||
|
rss_sync_interval: 1,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetAllIndexerSettings.into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
|
||||||
|
IndexerSettings {
|
||||||
|
id: 1,
|
||||||
|
maximum_size: 2,
|
||||||
|
minimum_age: 2,
|
||||||
|
retention: 2,
|
||||||
|
rss_sync_interval: 2,
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings {
|
||||||
|
maximum_size: Some(1),
|
||||||
|
minimum_age: Some(1),
|
||||||
|
retention: Some(1),
|
||||||
|
rss_sync_interval: Some(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrEditCommandHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
edit_all_indexer_settings_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_indexer_command() {
|
||||||
|
let expected_edit_indexer_params = EditIndexerParams {
|
||||||
|
indexer_id: 1,
|
||||||
|
name: Some("Test".to_owned()),
|
||||||
|
enable_rss: Some(true),
|
||||||
|
enable_automatic_search: Some(true),
|
||||||
|
enable_interactive_search: Some(true),
|
||||||
|
url: Some("http://test.com".to_owned()),
|
||||||
|
api_key: Some("testKey".to_owned()),
|
||||||
|
seed_ratio: Some("1.2".to_owned()),
|
||||||
|
tags: Some(vec![1, 2]),
|
||||||
|
priority: Some(25),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let edit_indexer_command = SonarrEditCommand::Indexer {
|
||||||
|
indexer_id: 1,
|
||||||
|
name: Some("Test".to_owned()),
|
||||||
|
enable_rss: true,
|
||||||
|
disable_rss: false,
|
||||||
|
enable_automatic_search: true,
|
||||||
|
disable_automatic_search: false,
|
||||||
|
enable_interactive_search: true,
|
||||||
|
disable_interactive_search: false,
|
||||||
|
url: Some("http://test.com".to_owned()),
|
||||||
|
api_key: Some("testKey".to_owned()),
|
||||||
|
seed_ratio: Some("1.2".to_owned()),
|
||||||
|
tag: Some(vec![1, 2]),
|
||||||
|
priority: Some(25),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_series_command() {
|
||||||
|
let expected_edit_series_params = EditSeriesParams {
|
||||||
|
series_id: 1,
|
||||||
|
monitored: Some(true),
|
||||||
|
use_season_folders: Some(true),
|
||||||
|
series_type: Some(SeriesType::Anime),
|
||||||
|
quality_profile_id: Some(1),
|
||||||
|
language_profile_id: Some(1),
|
||||||
|
root_folder_path: Some("/nfs/test".to_owned()),
|
||||||
|
tags: Some(vec![1, 2]),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let edit_series_command = SonarrEditCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
enable_monitoring: true,
|
||||||
|
disable_monitoring: false,
|
||||||
|
enable_season_folders: true,
|
||||||
|
disable_season_folders: false,
|
||||||
|
series_type: Some(SeriesType::Anime),
|
||||||
|
quality_profile_id: Some(1),
|
||||||
|
language_profile_id: Some(1),
|
||||||
|
root_folder_path: Some("/nfs/test".to_owned()),
|
||||||
|
tag: Some(vec![1, 2]),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_series_command_handles_disable_monitoring_flag_properly() {
|
||||||
|
let expected_edit_series_params = EditSeriesParams {
|
||||||
|
series_id: 1,
|
||||||
|
monitored: Some(false),
|
||||||
|
use_season_folders: Some(false),
|
||||||
|
series_type: Some(SeriesType::Anime),
|
||||||
|
quality_profile_id: Some(1),
|
||||||
|
language_profile_id: Some(1),
|
||||||
|
root_folder_path: Some("/nfs/test".to_owned()),
|
||||||
|
tags: Some(vec![1, 2]),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let edit_series_command = SonarrEditCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
enable_monitoring: false,
|
||||||
|
disable_monitoring: true,
|
||||||
|
enable_season_folders: false,
|
||||||
|
disable_season_folders: true,
|
||||||
|
series_type: Some(SeriesType::Anime),
|
||||||
|
quality_profile_id: Some(1),
|
||||||
|
language_profile_id: Some(1),
|
||||||
|
root_folder_path: Some("/nfs/test".to_owned()),
|
||||||
|
tag: Some(vec![1, 2]),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_edit_series_command_no_monitoring_boolean_flags_returns_none_value() {
|
||||||
|
let expected_edit_series_params = EditSeriesParams {
|
||||||
|
series_id: 1,
|
||||||
|
monitored: None,
|
||||||
|
use_season_folders: None,
|
||||||
|
series_type: Some(SeriesType::Anime),
|
||||||
|
quality_profile_id: Some(1),
|
||||||
|
language_profile_id: Some(1),
|
||||||
|
root_folder_path: Some("/nfs/test".to_owned()),
|
||||||
|
tags: Some(vec![1, 2]),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let edit_series_command = SonarrEditCommand::Series {
|
||||||
|
series_id: 1,
|
||||||
|
enable_monitoring: false,
|
||||||
|
disable_monitoring: false,
|
||||||
|
enable_season_folders: false,
|
||||||
|
disable_season_folders: false,
|
||||||
|
series_type: Some(SeriesType::Anime),
|
||||||
|
quality_profile_id: Some(1),
|
||||||
|
language_profile_id: Some(1),
|
||||||
|
root_folder_path: Some("/nfs/test".to_owned()),
|
||||||
|
tag: Some(vec![1, 2]),
|
||||||
|
clear_tags: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "get_command_handler_tests.rs"]
|
||||||
|
mod get_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrGetCommand {
|
||||||
|
#[command(about = "Get the shared settings for all indexers")]
|
||||||
|
AllIndexerSettings,
|
||||||
|
#[command(about = "Get detailed information for the episode with the given ID")]
|
||||||
|
EpisodeDetails {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the episode whose details you wish to fetch",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
episode_id: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "Fetch the host config for your Sonarr instance")]
|
||||||
|
HostConfig,
|
||||||
|
#[command(about = "Fetch the security config for your Sonarr instance")]
|
||||||
|
SecurityConfig,
|
||||||
|
#[command(about = "Get detailed information for the series with the given ID")]
|
||||||
|
SeriesDetails {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the series whose details you wish to fetch",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "Get the system status")]
|
||||||
|
SystemStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrGetCommand> for Command {
|
||||||
|
fn from(value: SonarrGetCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::Get(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrGetCommandHandler<'a, 'b> {
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrGetCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHandler<'a, 'b> {
|
||||||
|
fn with(
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrGetCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrGetCommandHandler {
|
||||||
|
_app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrGetCommand::AllIndexerSettings => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetAllIndexerSettings.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrGetCommand::EpisodeDetails { episode_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetEpisodeDetails(Some(episode_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrGetCommand::HostConfig => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetHostConfig.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrGetCommand::SecurityConfig => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetSecurityConfig.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrGetCommand::SeriesDetails { series_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetSeriesDetails(Some(series_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrGetCommand::SystemStatus => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetStatus.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cli::{
|
||||||
|
sonarr::{get_command_handler::SonarrGetCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
use crate::Cli;
|
||||||
|
use clap::CommandFactory;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_get_command_from() {
|
||||||
|
let command = SonarrGetCommand::SystemStatus;
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Sonarr(SonarrCommand::Get(command)));
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use clap::error::ErrorKind;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_indexer_settings_has_no_arg_requirements() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "all-indexer-settings"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_status_has_no_arg_requirements() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "system-status"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_episode_details_requires_episode_id() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "episode-details"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_episode_details_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"get",
|
||||||
|
"episode-details",
|
||||||
|
"--episode-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_host_config_has_no_arg_requirements() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "host-config"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_security_config_has_no_arg_requirements() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "security-config"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_details_requires_series_id() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "series-details"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_details_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"get",
|
||||||
|
"series-details",
|
||||||
|
"--series-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::{
|
||||||
|
sonarr::get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler},
|
||||||
|
CliCommandHandler,
|
||||||
|
},
|
||||||
|
models::{sonarr_models::SonarrSerdeable, Serdeable},
|
||||||
|
network::{sonarr_network::SonarrEvent, 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>(
|
||||||
|
SonarrEvent::GetAllIndexerSettings.into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let get_all_indexer_settings_command = SonarrGetCommand::AllIndexerSettings;
|
||||||
|
|
||||||
|
let result = SonarrGetCommandHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
get_all_indexer_settings_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_episode_details_command() {
|
||||||
|
let expected_episode_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetEpisodeDetails(Some(expected_episode_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let get_episode_details_command = SonarrGetCommand::EpisodeDetails { episode_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrGetCommandHandler::with(&app_arc, get_episode_details_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_host_config_command() {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(SonarrEvent::GetHostConfig.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let get_host_config_command = SonarrGetCommand::HostConfig;
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_security_config_command() {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(SonarrEvent::GetSecurityConfig.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let get_security_config_command = SonarrGetCommand::SecurityConfig;
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_series_details_command() {
|
||||||
|
let expected_series_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetSeriesDetails(Some(expected_series_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let get_series_details_command = SonarrGetCommand::SeriesDetails { series_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrGetCommandHandler::with(&app_arc, get_series_details_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>(SonarrEvent::GetStatus.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let get_system_status_command = SonarrGetCommand::SystemStatus;
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "list_command_handler_tests.rs"]
|
||||||
|
mod list_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrListCommand {
|
||||||
|
#[command(about = "List all items in the Sonarr blocklist")]
|
||||||
|
Blocklist,
|
||||||
|
#[command(about = "List all active downloads in Sonarr")]
|
||||||
|
Downloads,
|
||||||
|
#[command(about = "List disk space details for all provisioned root folders in Sonarr")]
|
||||||
|
DiskSpace,
|
||||||
|
#[command(about = "List the episodes for the series with the given ID")]
|
||||||
|
Episodes {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the series whose episodes you wish to fetch",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "Fetch all history events for the episode with the given ID")]
|
||||||
|
EpisodeHistory {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the episode whose history you wish to fetch",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
episode_id: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "Fetch all Sonarr history events")]
|
||||||
|
History {
|
||||||
|
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
|
||||||
|
events: u64,
|
||||||
|
},
|
||||||
|
#[command(about = "List all Sonarr indexers")]
|
||||||
|
Indexers,
|
||||||
|
#[command(about = "List all Sonarr language profiles")]
|
||||||
|
LanguageProfiles,
|
||||||
|
#[command(about = "Fetch Sonarr 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 Sonarr quality profiles")]
|
||||||
|
QualityProfiles,
|
||||||
|
#[command(about = "List all queued events")]
|
||||||
|
QueuedEvents,
|
||||||
|
#[command(about = "List all root folders in Sonarr")]
|
||||||
|
RootFolders,
|
||||||
|
#[command(about = "List all series in your Sonarr library")]
|
||||||
|
Series,
|
||||||
|
#[command(about = "Fetch all history events for the series with the given ID")]
|
||||||
|
SeriesHistory {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the series whose history you wish to fetch",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "List all Sonarr tags")]
|
||||||
|
Tags,
|
||||||
|
#[command(about = "List all Sonarr tasks")]
|
||||||
|
Tasks,
|
||||||
|
#[command(about = "List all Sonarr updates")]
|
||||||
|
Updates,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrListCommand> for Command {
|
||||||
|
fn from(value: SonarrListCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::List(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrListCommandHandler<'a, 'b> {
|
||||||
|
app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrListCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandHandler<'a, 'b> {
|
||||||
|
fn with(
|
||||||
|
app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrListCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrListCommandHandler {
|
||||||
|
app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrListCommand::Blocklist => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetBlocklist.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::Downloads => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetDownloads.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::DiskSpace => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetDiskSpace.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::Episodes { series_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetEpisodes(Some(series_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::EpisodeHistory { episode_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetEpisodeHistory(Some(episode_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::History { events: items } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetHistory(Some(items)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::Indexers => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetIndexers.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::LanguageProfiles => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetLanguageProfiles.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::Logs {
|
||||||
|
events,
|
||||||
|
output_in_log_format,
|
||||||
|
} => {
|
||||||
|
let logs = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetLogs(Some(events)).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if output_in_log_format {
|
||||||
|
let log_lines = self.app.lock().await.data.sonarr_data.logs.items.clone();
|
||||||
|
|
||||||
|
serde_json::to_string_pretty(&log_lines)?
|
||||||
|
} else {
|
||||||
|
serde_json::to_string_pretty(&logs)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SonarrListCommand::QualityProfiles => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetQualityProfiles.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::QueuedEvents => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetQueuedEvents.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::RootFolders => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetRootFolders.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::Series => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::ListSeries.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::SeriesHistory { series_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetSeriesHistory(Some(series_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::Tags => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetTags.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::Tasks => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetTasks.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrListCommand::Updates => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetUpdates.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cli::{
|
||||||
|
sonarr::{list_command_handler::SonarrListCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
use crate::Cli;
|
||||||
|
use clap::CommandFactory;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_list_command_from() {
|
||||||
|
let command = SonarrListCommand::Series;
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Sonarr(SonarrCommand::List(command)));
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use super::*;
|
||||||
|
use clap::{error::ErrorKind, Parser};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_list_commands_have_no_arg_requirements(
|
||||||
|
#[values(
|
||||||
|
"blocklist",
|
||||||
|
"series",
|
||||||
|
"downloads",
|
||||||
|
"disk-space",
|
||||||
|
"quality-profiles",
|
||||||
|
"indexers",
|
||||||
|
"queued-events",
|
||||||
|
"root-folders",
|
||||||
|
"tags",
|
||||||
|
"tasks",
|
||||||
|
"updates",
|
||||||
|
"language-profiles"
|
||||||
|
)]
|
||||||
|
subcommand: &str,
|
||||||
|
) {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", subcommand]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_episodes_requires_series_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episodes"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_episode_history_requires_series_id() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episode-history"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_episode_history_success() {
|
||||||
|
let expected_args = SonarrListCommand::EpisodeHistory { episode_id: 1 };
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"list",
|
||||||
|
"episode-history",
|
||||||
|
"--episode-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::List(episode_history_command))) =
|
||||||
|
result.unwrap().command
|
||||||
|
{
|
||||||
|
assert_eq!(episode_history_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_history_events_flag_requires_arguments() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "history", "--events"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_history_default_values() {
|
||||||
|
let expected_args = SonarrListCommand::History { events: 500 };
|
||||||
|
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "history"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::List(history_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(history_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_logs_events_flag_requires_arguments() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "logs", "--events"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_logs_default_values() {
|
||||||
|
let expected_args = SonarrListCommand::Logs {
|
||||||
|
events: 500,
|
||||||
|
output_in_log_format: false,
|
||||||
|
};
|
||||||
|
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "logs"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::List(logs_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(logs_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_episodes_success() {
|
||||||
|
let expected_args = SonarrListCommand::Episodes { series_id: 1 };
|
||||||
|
let result =
|
||||||
|
Cli::try_parse_from(["managarr", "sonarr", "list", "episodes", "--series-id", "1"]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::List(episodes_command))) = result.unwrap().command
|
||||||
|
{
|
||||||
|
assert_eq!(episodes_command, expected_args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_series_history_requires_series_id() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series-history"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_series_history_success() {
|
||||||
|
let expected_args = SonarrListCommand::SeriesHistory { series_id: 1 };
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"list",
|
||||||
|
"series-history",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::List(series_command))) = result.unwrap().command {
|
||||||
|
assert_eq!(series_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::sonarr::list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
|
||||||
|
use crate::cli::CliCommandHandler;
|
||||||
|
use crate::models::sonarr_models::SonarrSerdeable;
|
||||||
|
use crate::models::Serdeable;
|
||||||
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
network::{MockNetworkTrait, NetworkEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(SonarrListCommand::Blocklist, SonarrEvent::GetBlocklist)]
|
||||||
|
#[case(SonarrListCommand::Downloads, SonarrEvent::GetDownloads)]
|
||||||
|
#[case(SonarrListCommand::DiskSpace, SonarrEvent::GetDiskSpace)]
|
||||||
|
#[case(SonarrListCommand::Indexers, SonarrEvent::GetIndexers)]
|
||||||
|
#[case(SonarrListCommand::QualityProfiles, SonarrEvent::GetQualityProfiles)]
|
||||||
|
#[case(SonarrListCommand::QueuedEvents, SonarrEvent::GetQueuedEvents)]
|
||||||
|
#[case(SonarrListCommand::RootFolders, SonarrEvent::GetRootFolders)]
|
||||||
|
#[case(SonarrListCommand::Series, SonarrEvent::ListSeries)]
|
||||||
|
#[case(SonarrListCommand::Tags, SonarrEvent::GetTags)]
|
||||||
|
#[case(SonarrListCommand::Tasks, SonarrEvent::GetTasks)]
|
||||||
|
#[case(SonarrListCommand::Updates, SonarrEvent::GetUpdates)]
|
||||||
|
#[case(SonarrListCommand::LanguageProfiles, SonarrEvent::GetLanguageProfiles)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_list_command(
|
||||||
|
#[case] list_command: SonarrListCommand,
|
||||||
|
#[case] expected_sonarr_event: SonarrEvent,
|
||||||
|
) {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
|
||||||
|
let result = SonarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_list_episodes_command() {
|
||||||
|
let expected_series_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetEpisodes(Some(expected_series_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let list_episodes_command = SonarrListCommand::Episodes { series_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrListCommandHandler::with(&app_arc, list_episodes_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_list_history_command() {
|
||||||
|
let expected_events = 1000;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetHistory(Some(expected_events)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let list_history_command = SonarrListCommand::History { events: 1000 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrListCommandHandler::with(&app_arc, list_history_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>(
|
||||||
|
SonarrEvent::GetLogs(Some(expected_events)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let list_logs_command = SonarrListCommand::Logs {
|
||||||
|
events: 1000,
|
||||||
|
output_in_log_format: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_list_series_history_command() {
|
||||||
|
let expected_series_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetSeriesHistory(Some(expected_series_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let list_series_history_command = SonarrListCommand::SeriesHistory { series_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrListCommandHandler::with(&app_arc, list_series_history_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_list_episode_history_command() {
|
||||||
|
let expected_episode_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetEpisodeHistory(Some(expected_episode_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let list_episode_history_command = SonarrListCommand::EpisodeHistory { episode_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrListCommandHandler::with(&app_arc, list_episode_history_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "manual_search_command_handler_tests.rs"]
|
||||||
|
mod manual_search_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrManualSearchCommand {
|
||||||
|
#[command(about = "Trigger a manual search of releases for the episode with the given ID")]
|
||||||
|
Episode {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the episode whose releases you wish to fetch and list",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
episode_id: i64,
|
||||||
|
},
|
||||||
|
#[command(
|
||||||
|
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID.\nNote that when downloading a season release, ensure that the release includes 'fullSeason: true', otherwise you'll run into issues"
|
||||||
|
)]
|
||||||
|
Season {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the series whose releases you wish to fetch and list",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
#[arg(long, help = "The season number to search for", required = true)]
|
||||||
|
season_number: i64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrManualSearchCommand> for Command {
|
||||||
|
fn from(value: SonarrManualSearchCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::ManualSearch(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrManualSearchCommandHandler<'a, 'b> {
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrManualSearchCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
|
||||||
|
for SonarrManualSearchCommandHandler<'a, 'b>
|
||||||
|
{
|
||||||
|
fn with(
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrManualSearchCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrManualSearchCommandHandler {
|
||||||
|
_app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrManualSearchCommand::Episode { episode_id } => {
|
||||||
|
println!("Searching for episode releases. This may take a minute...");
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetEpisodeReleases(Some(episode_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrManualSearchCommand::Season {
|
||||||
|
series_id,
|
||||||
|
season_number,
|
||||||
|
} => {
|
||||||
|
println!("Searching for season releases. This may take a minute...");
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(
|
||||||
|
SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cli::{
|
||||||
|
sonarr::{manual_search_command_handler::SonarrManualSearchCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
use crate::Cli;
|
||||||
|
use clap::CommandFactory;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_manual_search_command_from() {
|
||||||
|
let command = SonarrManualSearchCommand::Episode { episode_id: 1 };
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Command::Sonarr(SonarrCommand::ManualSearch(command))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use super::*;
|
||||||
|
use clap::error::ErrorKind;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_manual_season_search_requires_series_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"manual-search",
|
||||||
|
"season",
|
||||||
|
"--season-number",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_manual_season_search_requires_season_number() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"manual-search",
|
||||||
|
"season",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_manual_season_search_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"manual-search",
|
||||||
|
"season",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--season-number",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_manual_episode_search_requires_episode_id() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "manual-search", "episode"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_manual_episode_search_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"manual-search",
|
||||||
|
"episode",
|
||||||
|
"--episode-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::{
|
||||||
|
sonarr::manual_search_command_handler::{
|
||||||
|
SonarrManualSearchCommand, SonarrManualSearchCommandHandler,
|
||||||
|
},
|
||||||
|
CliCommandHandler,
|
||||||
|
},
|
||||||
|
models::{sonarr_models::SonarrSerdeable, Serdeable},
|
||||||
|
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_manual_episode_search_command() {
|
||||||
|
let expected_episode_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 };
|
||||||
|
|
||||||
|
let result = SonarrManualSearchCommandHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
manual_episode_search_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_manual_season_search_command() {
|
||||||
|
let expected_series_id = 1;
|
||||||
|
let expected_season_number = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetSeasonReleases(Some((expected_series_id, expected_season_number))).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let manual_season_search_command = SonarrManualSearchCommand::Season {
|
||||||
|
series_id: 1,
|
||||||
|
season_number: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrManualSearchCommandHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
manual_season_search_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use add_command_handler::{SonarrAddCommand, SonarrAddCommandHandler};
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler};
|
||||||
|
use download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler};
|
||||||
|
use edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler};
|
||||||
|
use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler};
|
||||||
|
use list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
|
||||||
|
use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler};
|
||||||
|
use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use trigger_automatic_search_command_handler::{
|
||||||
|
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
models::sonarr_models::SonarrTaskName,
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{CliCommandHandler, Command};
|
||||||
|
|
||||||
|
mod add_command_handler;
|
||||||
|
mod delete_command_handler;
|
||||||
|
mod download_command_handler;
|
||||||
|
mod edit_command_handler;
|
||||||
|
mod get_command_handler;
|
||||||
|
mod list_command_handler;
|
||||||
|
mod manual_search_command_handler;
|
||||||
|
mod refresh_command_handler;
|
||||||
|
mod trigger_automatic_search_command_handler;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "sonarr_command_tests.rs"]
|
||||||
|
mod sonarr_command_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrCommand {
|
||||||
|
#[command(
|
||||||
|
subcommand,
|
||||||
|
about = "Commands to add or create new resources within your Sonarr instance"
|
||||||
|
)]
|
||||||
|
Add(SonarrAddCommand),
|
||||||
|
#[command(
|
||||||
|
subcommand,
|
||||||
|
about = "Commands to delete resources from your Sonarr instance"
|
||||||
|
)]
|
||||||
|
Delete(SonarrDeleteCommand),
|
||||||
|
#[command(
|
||||||
|
subcommand,
|
||||||
|
about = "Commands to edit resources in your Sonarr instance"
|
||||||
|
)]
|
||||||
|
Edit(SonarrEditCommand),
|
||||||
|
#[command(
|
||||||
|
subcommand,
|
||||||
|
about = "Commands to fetch details of the resources in your Sonarr instance"
|
||||||
|
)]
|
||||||
|
Get(SonarrGetCommand),
|
||||||
|
#[command(
|
||||||
|
subcommand,
|
||||||
|
about = "Commands to download releases in your Sonarr instance"
|
||||||
|
)]
|
||||||
|
Download(SonarrDownloadCommand),
|
||||||
|
#[command(
|
||||||
|
subcommand,
|
||||||
|
about = "Commands to list attributes from your Sonarr instance"
|
||||||
|
)]
|
||||||
|
List(SonarrListCommand),
|
||||||
|
#[command(
|
||||||
|
subcommand,
|
||||||
|
about = "Commands to refresh the data in your Sonarr instance"
|
||||||
|
)]
|
||||||
|
Refresh(SonarrRefreshCommand),
|
||||||
|
#[command(subcommand, about = "Commands to manually search for releases")]
|
||||||
|
ManualSearch(SonarrManualSearchCommand),
|
||||||
|
#[command(
|
||||||
|
subcommand,
|
||||||
|
about = "Commands to trigger automatic searches for releases of different resources in your Sonarr instance"
|
||||||
|
)]
|
||||||
|
TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand),
|
||||||
|
#[command(about = "Clear the blocklist")]
|
||||||
|
ClearBlocklist,
|
||||||
|
#[command(about = "Mark the Sonarr history item with the given ID as 'failed'")]
|
||||||
|
MarkHistoryItemAsFailed {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the history item you wish to mark as 'failed'",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
history_item_id: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "Search for a new series to add to Sonarr")]
|
||||||
|
SearchNewSeries {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The title of the series you want to search for",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
query: String,
|
||||||
|
},
|
||||||
|
#[command(about = "Start the specified Sonarr task")]
|
||||||
|
StartTask {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The name of the task to trigger",
|
||||||
|
value_enum,
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
task_name: SonarrTaskName,
|
||||||
|
},
|
||||||
|
#[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 Sonarr indexers")]
|
||||||
|
TestAllIndexers,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrCommand> for Command {
|
||||||
|
fn from(sonarr_command: SonarrCommand) -> Command {
|
||||||
|
Command::Sonarr(sonarr_command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrCliHandler<'a, 'b> {
|
||||||
|
app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, 'b> {
|
||||||
|
fn with(
|
||||||
|
app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrCliHandler {
|
||||||
|
app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrCommand::Add(add_command) => {
|
||||||
|
SonarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::Delete(delete_command) => {
|
||||||
|
SonarrDeleteCommandHandler::with(self.app, delete_command, self.network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::Edit(edit_command) => {
|
||||||
|
SonarrEditCommandHandler::with(self.app, edit_command, self.network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::Download(download_command) => {
|
||||||
|
SonarrDownloadCommandHandler::with(self.app, download_command, self.network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::Get(get_command) => {
|
||||||
|
SonarrGetCommandHandler::with(self.app, get_command, self.network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::List(list_command) => {
|
||||||
|
SonarrListCommandHandler::with(self.app, list_command, self.network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::Refresh(refresh_command) => {
|
||||||
|
SonarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::ManualSearch(manual_search_command) => {
|
||||||
|
SonarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
|
||||||
|
SonarrTriggerAutomaticSearchCommandHandler::with(
|
||||||
|
self.app,
|
||||||
|
trigger_automatic_search_command,
|
||||||
|
self.network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
SonarrCommand::ClearBlocklist => {
|
||||||
|
self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::GetBlocklist.into())
|
||||||
|
.await?;
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::ClearBlocklist.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
|
||||||
|
let _ = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
|
||||||
|
.await?;
|
||||||
|
"Sonarr history item marked as 'failed'".to_owned()
|
||||||
|
}
|
||||||
|
SonarrCommand::SearchNewSeries { query } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::SearchNewSeries(Some(query)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrCommand::StartTask { task_name } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::StartTask(Some(task_name)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrCommand::TestIndexer { indexer_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::TestIndexer(Some(indexer_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrCommand::TestAllIndexers => {
|
||||||
|
println!("Testing all Sonarr indexers. This may take a minute...");
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::TestAllIndexers.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "refresh_command_handler_tests.rs"]
|
||||||
|
mod refresh_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrRefreshCommand {
|
||||||
|
#[command(about = "Refresh all series data for all series in your Sonarr library")]
|
||||||
|
AllSeries,
|
||||||
|
#[command(about = "Refresh series data and scan disk for the series with the given ID")]
|
||||||
|
Series {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The ID of the series to refresh information on and to scan the disk for",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "Refresh all downloads in Sonarr")]
|
||||||
|
Downloads,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrRefreshCommand> for Command {
|
||||||
|
fn from(value: SonarrRefreshCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::Refresh(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrRefreshCommandHandler<'a, 'b> {
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrRefreshCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrRefreshCommand>
|
||||||
|
for SonarrRefreshCommandHandler<'a, 'b>
|
||||||
|
{
|
||||||
|
fn with(
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrRefreshCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrRefreshCommandHandler {
|
||||||
|
_app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> anyhow::Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrRefreshCommand::AllSeries => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::UpdateAllSeries.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrRefreshCommand::Series { series_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::UpdateAndScanSeries(Some(series_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrRefreshCommand::Downloads => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::UpdateDownloads.into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use crate::cli::{
|
||||||
|
sonarr::{refresh_command_handler::SonarrRefreshCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
use crate::Cli;
|
||||||
|
use clap::CommandFactory;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_refresh_command_from() {
|
||||||
|
let command = SonarrRefreshCommand::AllSeries;
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Sonarr(SonarrCommand::Refresh(command)));
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use super::*;
|
||||||
|
use clap::{error::ErrorKind, Parser};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_refresh_commands_have_no_arg_requirements(
|
||||||
|
#[values("all-series", "downloads")] subcommand: &str,
|
||||||
|
) {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "refresh", subcommand]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_refresh_series_requires_series_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "refresh", "series"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_refresh_series_success() {
|
||||||
|
let expected_args = SonarrRefreshCommand::Series { series_id: 1 };
|
||||||
|
let result = Cli::try_parse_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"refresh",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Some(Command::Sonarr(SonarrCommand::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::{app::App, cli::sonarr::refresh_command_handler::SonarrRefreshCommandHandler};
|
||||||
|
use crate::{
|
||||||
|
cli::{sonarr::refresh_command_handler::SonarrRefreshCommand, CliCommandHandler},
|
||||||
|
network::sonarr_network::SonarrEvent,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
models::{sonarr_models::SonarrSerdeable, Serdeable},
|
||||||
|
network::{MockNetworkTrait, NetworkEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(SonarrRefreshCommand::AllSeries, SonarrEvent::UpdateAllSeries)]
|
||||||
|
#[case(SonarrRefreshCommand::Downloads, SonarrEvent::UpdateDownloads)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_refresh_command(
|
||||||
|
#[case] refresh_command: SonarrRefreshCommand,
|
||||||
|
#[case] expected_sonarr_event: SonarrEvent,
|
||||||
|
) {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
|
||||||
|
let result = SonarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_refresh_series_command() {
|
||||||
|
let expected_series_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let refresh_series_command = SonarrRefreshCommand::Series { series_id: 1 };
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrRefreshCommandHandler::with(&app_arc, refresh_series_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,620 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cli::{
|
||||||
|
sonarr::{list_command_handler::SonarrListCommand, SonarrCommand},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
use crate::Cli;
|
||||||
|
use clap::CommandFactory;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_command_from() {
|
||||||
|
let command = SonarrCommand::List(SonarrListCommand::Series);
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Sonarr(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use super::*;
|
||||||
|
use clap::error::ErrorKind;
|
||||||
|
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", "sonarr", subcommand]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mark_history_item_as_failed_requires_history_item_id() {
|
||||||
|
let result =
|
||||||
|
Cli::command().try_get_matches_from(["managarr", "sonarr", "mark-history-item-as-failed"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mark_history_item_as_failed_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"mark-history-item-as-failed",
|
||||||
|
"--history-item-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_search_new_series_requires_query() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "search-new-series"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_search_new_series_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"search-new-series",
|
||||||
|
"--query",
|
||||||
|
"halo",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_start_task_requires_task_name() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "start-task"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_start_task_task_name_validation() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"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",
|
||||||
|
"sonarr",
|
||||||
|
"start-task",
|
||||||
|
"--task-name",
|
||||||
|
"application-update-check",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_test_indexer_requires_indexer_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "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",
|
||||||
|
"sonarr",
|
||||||
|
"test-indexer",
|
||||||
|
"--indexer-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::{
|
||||||
|
sonarr::{
|
||||||
|
add_command_handler::SonarrAddCommand, delete_command_handler::SonarrDeleteCommand,
|
||||||
|
download_command_handler::SonarrDownloadCommand, edit_command_handler::SonarrEditCommand,
|
||||||
|
get_command_handler::SonarrGetCommand, list_command_handler::SonarrListCommand,
|
||||||
|
manual_search_command_handler::SonarrManualSearchCommand,
|
||||||
|
refresh_command_handler::SonarrRefreshCommand,
|
||||||
|
trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand,
|
||||||
|
SonarrCliHandler, SonarrCommand,
|
||||||
|
},
|
||||||
|
CliCommandHandler,
|
||||||
|
},
|
||||||
|
models::{
|
||||||
|
sonarr_models::{
|
||||||
|
BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody,
|
||||||
|
SonarrSerdeable, SonarrTaskName,
|
||||||
|
},
|
||||||
|
Serdeable,
|
||||||
|
},
|
||||||
|
network::{sonarr_network::SonarrEvent, 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>(SonarrEvent::GetBlocklist.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::BlocklistResponse(
|
||||||
|
BlocklistResponse {
|
||||||
|
records: vec![BlocklistItem::default()],
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(SonarrEvent::ClearBlocklist.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let claer_blocklist_command = SonarrCommand::ClearBlocklist;
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mark_history_item_as_failed_command() {
|
||||||
|
let expected_history_item_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let mark_history_item_as_failed_command =
|
||||||
|
SonarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 };
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
mark_history_item_as_failed_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_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>(
|
||||||
|
SonarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let add_tag_command = SonarrCommand::Add(SonarrAddCommand::Tag {
|
||||||
|
name: expected_tag_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_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>(
|
||||||
|
SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let delete_blocklist_item_command =
|
||||||
|
SonarrCommand::Delete(SonarrDeleteCommand::BlocklistItem {
|
||||||
|
blocklist_item_id: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrCliHandler::with(&app_arc, delete_blocklist_item_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_cli_handler_delegates_download_commands_to_the_download_command_handler() {
|
||||||
|
let expected_params = SonarrReleaseDownloadBody {
|
||||||
|
guid: "1234".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
series_id: Some(1),
|
||||||
|
..SonarrReleaseDownloadBody::default()
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::DownloadRelease(expected_params).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let download_series_release_command =
|
||||||
|
SonarrCommand::Download(SonarrDownloadCommand::Series {
|
||||||
|
guid: "1234".to_owned(),
|
||||||
|
indexer_id: 1,
|
||||||
|
series_id: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrCliHandler::with(&app_arc, download_series_release_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
|
||||||
|
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||||
|
id: 1,
|
||||||
|
maximum_size: 1,
|
||||||
|
minimum_age: 1,
|
||||||
|
retention: 1,
|
||||||
|
rss_sync_interval: 1,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetAllIndexerSettings.into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
|
||||||
|
IndexerSettings {
|
||||||
|
id: 1,
|
||||||
|
maximum_size: 2,
|
||||||
|
minimum_age: 2,
|
||||||
|
retention: 2,
|
||||||
|
rss_sync_interval: 2,
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let edit_all_indexer_settings_command =
|
||||||
|
SonarrCommand::Edit(SonarrEditCommand::AllIndexerSettings {
|
||||||
|
maximum_size: Some(1),
|
||||||
|
minimum_age: Some(1),
|
||||||
|
retention: Some(1),
|
||||||
|
rss_sync_interval: Some(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
edit_all_indexer_settings_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler(
|
||||||
|
) {
|
||||||
|
let expected_episode_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let manual_episode_search_command =
|
||||||
|
SonarrCommand::ManualSearch(SonarrManualSearchCommand::Episode { episode_id: 1 });
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler(
|
||||||
|
) {
|
||||||
|
let expected_episode_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let manual_episode_search_command =
|
||||||
|
SonarrCommand::TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand::Episode {
|
||||||
|
episode_id: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result =
|
||||||
|
SonarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_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>(SonarrEvent::GetStatus.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let get_system_status_command = SonarrCommand::Get(SonarrGetCommand::SystemStatus);
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(SonarrEvent::ListSeries.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::SeriesVec(vec![
|
||||||
|
Series::default(),
|
||||||
|
])))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let list_series_command = SonarrCommand::List(SonarrListCommand::Series);
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(&app_arc, list_series_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sonarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
|
||||||
|
let expected_series_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let refresh_series_command =
|
||||||
|
SonarrCommand::Refresh(SonarrRefreshCommand::Series { series_id: 1 });
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(&app_arc, refresh_series_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_search_new_series_command() {
|
||||||
|
let expected_search_query = "halo".to_owned();
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::SearchNewSeries(Some(expected_search_query)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let search_new_series_command = SonarrCommand::SearchNewSeries {
|
||||||
|
query: "halo".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(&app_arc, search_new_series_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_start_task_command() {
|
||||||
|
let expected_task_name = SonarrTaskName::ApplicationUpdateCheck;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::StartTask(Some(expected_task_name)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let start_task_command = SonarrCommand::StartTask {
|
||||||
|
task_name: SonarrTaskName::ApplicationUpdateCheck,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::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>(
|
||||||
|
SonarrEvent::TestIndexer(Some(expected_indexer_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let test_indexer_command = SonarrCommand::TestIndexer { indexer_id: 1 };
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::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>(SonarrEvent::TestAllIndexers.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let test_all_indexers_command = SonarrCommand::TestAllIndexers;
|
||||||
|
|
||||||
|
let result = SonarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
cli::{CliCommandHandler, Command},
|
||||||
|
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SonarrCommand;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "trigger_automatic_search_command_handler_tests.rs"]
|
||||||
|
mod trigger_automatic_search_command_handler_tests;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||||
|
pub enum SonarrTriggerAutomaticSearchCommand {
|
||||||
|
#[command(about = "Trigger an automatic search for the series with the specified ID")]
|
||||||
|
Series {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The ID of the series you want to trigger an automatic search for",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
},
|
||||||
|
#[command(
|
||||||
|
about = "Trigger an automatic search for the given season corresponding to the series with the given ID"
|
||||||
|
)]
|
||||||
|
Season {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The Sonarr ID of the series whose season you wish to trigger an automatic search for",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
series_id: i64,
|
||||||
|
#[arg(long, help = "The season number to search for", required = true)]
|
||||||
|
season_number: i64,
|
||||||
|
},
|
||||||
|
#[command(about = "Trigger an automatic search for the episode with the specified ID")]
|
||||||
|
Episode {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "The ID of the episode you want to trigger an automatic search for",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
episode_id: i64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrTriggerAutomaticSearchCommand> for Command {
|
||||||
|
fn from(value: SonarrTriggerAutomaticSearchCommand) -> Self {
|
||||||
|
Command::Sonarr(SonarrCommand::TriggerAutomaticSearch(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct SonarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrTriggerAutomaticSearchCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand>
|
||||||
|
for SonarrTriggerAutomaticSearchCommandHandler<'a, 'b>
|
||||||
|
{
|
||||||
|
fn with(
|
||||||
|
_app: &'a Arc<Mutex<App<'b>>>,
|
||||||
|
command: SonarrTriggerAutomaticSearchCommand,
|
||||||
|
network: &'a mut dyn NetworkTrait,
|
||||||
|
) -> Self {
|
||||||
|
SonarrTriggerAutomaticSearchCommandHandler {
|
||||||
|
_app,
|
||||||
|
command,
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self) -> Result<String> {
|
||||||
|
let result = match self.command {
|
||||||
|
SonarrTriggerAutomaticSearchCommand::Series { series_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(series_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrTriggerAutomaticSearchCommand::Season {
|
||||||
|
series_id,
|
||||||
|
season_number,
|
||||||
|
} => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(
|
||||||
|
SonarrEvent::TriggerAutomaticSeasonSearch(Some((series_id, season_number))).into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
SonarrTriggerAutomaticSearchCommand::Episode { episode_id } => {
|
||||||
|
let resp = self
|
||||||
|
.network
|
||||||
|
.handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(Some(episode_id)).into())
|
||||||
|
.await?;
|
||||||
|
serde_json::to_string_pretty(&resp)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cli::{
|
||||||
|
sonarr::{
|
||||||
|
trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand, SonarrCommand,
|
||||||
|
},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
use crate::Cli;
|
||||||
|
use clap::CommandFactory;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_trigger_automatic_search_command_from() {
|
||||||
|
let command = SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 };
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Command::Sonarr(SonarrCommand::TriggerAutomaticSearch(command))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cli {
|
||||||
|
use super::*;
|
||||||
|
use clap::error::ErrorKind;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigger_automatic_series_search_requires_series_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"trigger-automatic-search",
|
||||||
|
"series",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigger_automatic_series_search_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"trigger-automatic-search",
|
||||||
|
"series",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigger_automatic_season_search_requires_series_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"trigger-automatic-search",
|
||||||
|
"season",
|
||||||
|
"--season-number",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigger_automatic_season_search_requires_season_number() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"trigger-automatic-search",
|
||||||
|
"season",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigger_automatic_season_search_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"trigger-automatic-search",
|
||||||
|
"season",
|
||||||
|
"--series-id",
|
||||||
|
"1",
|
||||||
|
"--season-number",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigger_automatic_episode_search_requires_episode_id() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"trigger-automatic-search",
|
||||||
|
"episode",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().kind(),
|
||||||
|
ErrorKind::MissingRequiredArgument
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigger_automatic_episode_search_requirements_satisfied() {
|
||||||
|
let result = Cli::command().try_get_matches_from([
|
||||||
|
"managarr",
|
||||||
|
"sonarr",
|
||||||
|
"trigger-automatic-search",
|
||||||
|
"episode",
|
||||||
|
"--episode-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::{
|
||||||
|
sonarr::trigger_automatic_search_command_handler::{
|
||||||
|
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
|
||||||
|
},
|
||||||
|
CliCommandHandler,
|
||||||
|
},
|
||||||
|
models::{sonarr_models::SonarrSerdeable, Serdeable},
|
||||||
|
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_trigger_automatic_series_search_command() {
|
||||||
|
let expected_series_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::TriggerAutomaticSeriesSearch(Some(expected_series_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let trigger_automatic_series_search_command =
|
||||||
|
SonarrTriggerAutomaticSearchCommand::Series { series_id: 1 };
|
||||||
|
|
||||||
|
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
trigger_automatic_series_search_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_trigger_automatic_season_search_command() {
|
||||||
|
let expected_series_id = 1;
|
||||||
|
let expected_season_number = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::TriggerAutomaticSeasonSearch(Some((
|
||||||
|
expected_series_id,
|
||||||
|
expected_season_number,
|
||||||
|
)))
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let trigger_automatic_season_search_command = SonarrTriggerAutomaticSearchCommand::Season {
|
||||||
|
series_id: 1,
|
||||||
|
season_number: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
trigger_automatic_season_search_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_trigger_automatic_episode_search_command() {
|
||||||
|
let expected_episode_id = 1;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let trigger_automatic_episode_search_command =
|
||||||
|
SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 };
|
||||||
|
|
||||||
|
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
trigger_automatic_episode_search_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,9 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler};
|
use crate::handlers::radarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler};
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{BlocklistItem, BlocklistItemMovie};
|
||||||
BlocklistItem, BlocklistItemMovie, Language, Quality, QualityWrapper,
|
|
||||||
};
|
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
|
||||||
|
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
|
|
||||||
mod test_handle_scroll_up_and_down {
|
mod test_handle_scroll_up_and_down {
|
||||||
@@ -960,6 +959,7 @@ mod tests {
|
|||||||
id: 3,
|
id: 3,
|
||||||
source_title: "test 1".to_owned(),
|
source_title: "test 1".to_owned(),
|
||||||
languages: vec![Language {
|
languages: vec![Language {
|
||||||
|
id: 1,
|
||||||
name: "telgu".to_owned(),
|
name: "telgu".to_owned(),
|
||||||
}],
|
}],
|
||||||
quality: QualityWrapper {
|
quality: QualityWrapper {
|
||||||
@@ -968,6 +968,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
custom_formats: Some(vec![Language {
|
custom_formats: Some(vec![Language {
|
||||||
|
id: 2,
|
||||||
name: "nikki".to_owned(),
|
name: "nikki".to_owned(),
|
||||||
}]),
|
}]),
|
||||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
|
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
|
||||||
@@ -980,6 +981,7 @@ mod tests {
|
|||||||
id: 2,
|
id: 2,
|
||||||
source_title: "test 2".to_owned(),
|
source_title: "test 2".to_owned(),
|
||||||
languages: vec![Language {
|
languages: vec![Language {
|
||||||
|
id: 3,
|
||||||
name: "chinese".to_owned(),
|
name: "chinese".to_owned(),
|
||||||
}],
|
}],
|
||||||
quality: QualityWrapper {
|
quality: QualityWrapper {
|
||||||
@@ -989,9 +991,11 @@ mod tests {
|
|||||||
},
|
},
|
||||||
custom_formats: Some(vec![
|
custom_formats: Some(vec![
|
||||||
Language {
|
Language {
|
||||||
|
id: 4,
|
||||||
name: "alex".to_owned(),
|
name: "alex".to_owned(),
|
||||||
},
|
},
|
||||||
Language {
|
Language {
|
||||||
|
id: 5,
|
||||||
name: "English".to_owned(),
|
name: "English".to_owned(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -1005,6 +1009,7 @@ mod tests {
|
|||||||
id: 1,
|
id: 1,
|
||||||
source_title: "test 3".to_owned(),
|
source_title: "test 3".to_owned(),
|
||||||
languages: vec![Language {
|
languages: vec![Language {
|
||||||
|
id: 1,
|
||||||
name: "english".to_owned(),
|
name: "english".to_owned(),
|
||||||
}],
|
}],
|
||||||
quality: QualityWrapper {
|
quality: QualityWrapper {
|
||||||
@@ -1013,6 +1018,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
custom_formats: Some(vec![Language {
|
custom_formats: Some(vec![Language {
|
||||||
|
id: 2,
|
||||||
name: "English".to_owned(),
|
name: "English".to_owned(),
|
||||||
}]),
|
}]),
|
||||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
|
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
|
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
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 strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
|
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
|
||||||
use crate::models::BlockSelectionState;
|
use crate::models::BlockSelectionState;
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ mod tests {
|
|||||||
use std::sync::atomic::Ordering;
|
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::modals::EditIndexerModal;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -334,7 +334,7 @@ mod tests {
|
|||||||
use std::sync::atomic::Ordering;
|
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::modals::EditIndexerModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||||
};
|
};
|
||||||
@@ -759,7 +759,7 @@ mod tests {
|
|||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, BlockSelectionState,
|
servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, BlockSelectionState,
|
||||||
};
|
};
|
||||||
@@ -1224,7 +1224,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
@@ -1281,7 +1281,7 @@ mod tests {
|
|||||||
|
|
||||||
mod test_handle_key_char {
|
mod test_handle_key_char {
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
|
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
|
||||||
use crate::models::BlockSelectionState;
|
use crate::models::BlockSelectionState;
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
|||||||
@@ -9,16 +9,15 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::indexers::IndexersHandler;
|
use crate::handlers::radarr_handlers::indexers::IndexersHandler;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::radarr_models::Indexer;
|
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_models::Indexer;
|
||||||
use crate::test_handler_delegation;
|
use crate::test_handler_delegation;
|
||||||
|
|
||||||
mod test_handle_scroll_up_and_down {
|
mod test_handle_scroll_up_and_down {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::models::radarr_models::Indexer;
|
|
||||||
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
|
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -65,7 +64,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod test_handle_home_end {
|
mod test_handle_home_end {
|
||||||
use crate::models::radarr_models::Indexer;
|
|
||||||
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
|
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -239,11 +237,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod test_handle_submit {
|
mod test_handle_submit {
|
||||||
use crate::models::radarr_models::{Indexer, IndexerField};
|
use crate::models::servarr_data::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::{
|
||||||
RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_models::{Indexer, IndexerField};
|
||||||
use bimap::BiMap;
|
use bimap::BiMap;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serde_json::{Number, Value};
|
use serde_json::{Number, Value};
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ use crate::models::servarr_data::radarr::radarr_data::{
|
|||||||
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||||
INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS,
|
INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::{BlockSelectionState, Scrollable};
|
use crate::models::BlockSelectionState;
|
||||||
|
use crate::models::Scrollable;
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
|
||||||
mod edit_indexer_handler;
|
mod edit_indexer_handler;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
|
use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
|
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
@@ -14,7 +14,7 @@ mod tests {
|
|||||||
use pretty_assertions::assert_str_eq;
|
use pretty_assertions::assert_str_eq;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
|
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use crate::simple_stateful_iterable_vec;
|
use crate::simple_stateful_iterable_vec;
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ mod tests {
|
|||||||
|
|
||||||
mod test_handle_home_end {
|
mod test_handle_home_end {
|
||||||
use crate::extended_stateful_iterable_vec;
|
use crate::extended_stateful_iterable_vec;
|
||||||
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
|
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use pretty_assertions::assert_str_eq;
|
use pretty_assertions::assert_str_eq;
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::library::add_movie_handler::AddMovieHandler;
|
use crate::handlers::radarr_handlers::library::add_movie_handler::AddMovieHandler;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{AddMovieSearchResult, MinimumAvailability, MovieMonitor};
|
||||||
AddMovieSearchResult, MinimumAvailability, Monitor, RootFolder,
|
|
||||||
};
|
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
|
||||||
|
use crate::models::servarr_models::RootFolder;
|
||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
|
|
||||||
mod test_handle_scroll_up_and_down {
|
mod test_handle_scroll_up_and_down {
|
||||||
@@ -142,7 +141,7 @@ mod tests {
|
|||||||
fn test_add_movie_select_monitor_scroll(
|
fn test_add_movie_select_monitor_scroll(
|
||||||
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
|
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
|
||||||
) {
|
) {
|
||||||
let monitor_vec = Vec::from_iter(Monitor::iter());
|
let monitor_vec = Vec::from_iter(MovieMonitor::iter());
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
||||||
app
|
app
|
||||||
@@ -535,7 +534,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_add_movie_select_monitor_home_end() {
|
fn test_add_movie_select_monitor_home_end() {
|
||||||
let monitor_vec = Vec::from_iter(Monitor::iter());
|
let monitor_vec = Vec::from_iter(MovieMonitor::iter());
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
||||||
app
|
app
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::library::{movies_sorting_options, LibraryHandler};
|
use crate::handlers::radarr_handlers::library::{movies_sorting_options, LibraryHandler};
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::radarr_models::{Language, Movie};
|
use crate::models::radarr_models::Movie;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, DELETE_MOVIE_BLOCKS, EDIT_MOVIE_BLOCKS, LIBRARY_BLOCKS,
|
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, DELETE_MOVIE_BLOCKS, EDIT_MOVIE_BLOCKS, LIBRARY_BLOCKS,
|
||||||
MOVIE_DETAILS_BLOCKS,
|
MOVIE_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_models::Language;
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
use crate::test_handler_delegation;
|
use crate::test_handler_delegation;
|
||||||
@@ -1806,6 +1807,7 @@ mod tests {
|
|||||||
id: 3,
|
id: 3,
|
||||||
title: "test 1".into(),
|
title: "test 1".into(),
|
||||||
original_language: Language {
|
original_language: Language {
|
||||||
|
id: 1,
|
||||||
name: "English".to_owned(),
|
name: "English".to_owned(),
|
||||||
},
|
},
|
||||||
size_on_disk: 1024,
|
size_on_disk: 1024,
|
||||||
@@ -1822,6 +1824,7 @@ mod tests {
|
|||||||
id: 2,
|
id: 2,
|
||||||
title: "test 2".into(),
|
title: "test 2".into(),
|
||||||
original_language: Language {
|
original_language: Language {
|
||||||
|
id: 2,
|
||||||
name: "Chinese".to_owned(),
|
name: "Chinese".to_owned(),
|
||||||
},
|
},
|
||||||
size_on_disk: 2048,
|
size_on_disk: 2048,
|
||||||
@@ -1838,6 +1841,7 @@ mod tests {
|
|||||||
id: 1,
|
id: 1,
|
||||||
title: "test 3".into(),
|
title: "test 3".into(),
|
||||||
original_language: Language {
|
original_language: Language {
|
||||||
|
id: 3,
|
||||||
name: "Japanese".to_owned(),
|
name: "Japanese".to_owned(),
|
||||||
},
|
},
|
||||||
size_on_disk: 512,
|
size_on_disk: 512,
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
|
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
|
||||||
use crate::models::radarr_models::{Language, Release};
|
use crate::models::radarr_models::RadarrRelease;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, EDIT_MOVIE_SELECTION_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
ActiveRadarrBlock, EDIT_MOVIE_SELECTION_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_models::Language;
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
use crate::models::{BlockSelectionState, Scrollable};
|
use crate::models::{BlockSelectionState, Scrollable};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
@@ -47,18 +48,28 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_ready(&self) -> bool {
|
fn is_ready(&self) -> bool {
|
||||||
let movie_details_modal_is_ready =
|
if let Some(movie_details_modal) = &self.app.data.radarr_data.movie_details_modal {
|
||||||
if let Some(movie_details_modal) = &self.app.data.radarr_data.movie_details_modal {
|
match self.active_radarr_block {
|
||||||
!movie_details_modal.movie_details.is_empty()
|
ActiveRadarrBlock::MovieDetails => {
|
||||||
|| !movie_details_modal.movie_history.is_empty()
|
!self.app.is_loading && !movie_details_modal.movie_details.is_empty()
|
||||||
|| !movie_details_modal.movie_cast.is_empty()
|
}
|
||||||
|| !movie_details_modal.movie_crew.is_empty()
|
ActiveRadarrBlock::MovieHistory => {
|
||||||
|| !movie_details_modal.movie_releases.is_empty()
|
!self.app.is_loading && !movie_details_modal.movie_history.is_empty()
|
||||||
} else {
|
}
|
||||||
false
|
ActiveRadarrBlock::Cast => {
|
||||||
};
|
!self.app.is_loading && !movie_details_modal.movie_cast.is_empty()
|
||||||
|
}
|
||||||
!self.app.is_loading && movie_details_modal_is_ready
|
ActiveRadarrBlock::Crew => {
|
||||||
|
!self.app.is_loading && !movie_details_modal.movie_crew.is_empty()
|
||||||
|
}
|
||||||
|
ActiveRadarrBlock::ManualSearch => {
|
||||||
|
!self.app.is_loading && !movie_details_modal.movie_releases.is_empty()
|
||||||
|
}
|
||||||
|
_ => !self.app.is_loading,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_scroll_up(&mut self) {
|
fn handle_scroll_up(&mut self) {
|
||||||
@@ -495,7 +506,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn releases_sorting_options() -> Vec<SortOption<Release>> {
|
fn releases_sorting_options() -> Vec<SortOption<RadarrRelease>> {
|
||||||
vec![
|
vec![
|
||||||
SortOption {
|
SortOption {
|
||||||
name: "Source",
|
name: "Source",
|
||||||
@@ -550,6 +561,7 @@ fn releases_sorting_options() -> Vec<SortOption<Release>> {
|
|||||||
name: "Language",
|
name: "Language",
|
||||||
cmp_fn: Some(|a, b| {
|
cmp_fn: Some(|a, b| {
|
||||||
let default_language_vec = vec![Language {
|
let default_language_vec = vec![Language {
|
||||||
|
id: 1,
|
||||||
name: "_".to_owned(),
|
name: "_".to_owned(),
|
||||||
}];
|
}];
|
||||||
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ mod tests {
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use pretty_assertions::assert_str_eq;
|
use pretty_assertions::assert_str_eq;
|
||||||
|
use rstest::rstest;
|
||||||
use serde_json::Number;
|
use serde_json::Number;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
@@ -13,11 +14,11 @@ mod tests {
|
|||||||
releases_sorting_options, MovieDetailsHandler,
|
releases_sorting_options, MovieDetailsHandler,
|
||||||
};
|
};
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::RadarrRelease;
|
||||||
Credit, Language, MovieHistoryItem, Quality, QualityWrapper, Release,
|
use crate::models::radarr_models::{Credit, MovieHistoryItem};
|
||||||
};
|
|
||||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
|
||||||
|
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||||
|
|
||||||
@@ -405,7 +406,7 @@ mod tests {
|
|||||||
movie_details_modal
|
movie_details_modal
|
||||||
.movie_releases
|
.movie_releases
|
||||||
.set_items(simple_stateful_iterable_vec!(
|
.set_items(simple_stateful_iterable_vec!(
|
||||||
Release,
|
RadarrRelease,
|
||||||
HorizontallyScrollableText
|
HorizontallyScrollableText
|
||||||
));
|
));
|
||||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||||
@@ -453,7 +454,7 @@ mod tests {
|
|||||||
movie_details_modal
|
movie_details_modal
|
||||||
.movie_releases
|
.movie_releases
|
||||||
.set_items(simple_stateful_iterable_vec!(
|
.set_items(simple_stateful_iterable_vec!(
|
||||||
Release,
|
RadarrRelease,
|
||||||
HorizontallyScrollableText
|
HorizontallyScrollableText
|
||||||
));
|
));
|
||||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||||
@@ -996,7 +997,7 @@ mod tests {
|
|||||||
movie_details_modal
|
movie_details_modal
|
||||||
.movie_releases
|
.movie_releases
|
||||||
.set_items(extended_stateful_iterable_vec!(
|
.set_items(extended_stateful_iterable_vec!(
|
||||||
Release,
|
RadarrRelease,
|
||||||
HorizontallyScrollableText
|
HorizontallyScrollableText
|
||||||
));
|
));
|
||||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||||
@@ -1054,7 +1055,7 @@ mod tests {
|
|||||||
movie_details_modal
|
movie_details_modal
|
||||||
.movie_releases
|
.movie_releases
|
||||||
.set_items(extended_stateful_iterable_vec!(
|
.set_items(extended_stateful_iterable_vec!(
|
||||||
Release,
|
RadarrRelease,
|
||||||
HorizontallyScrollableText
|
HorizontallyScrollableText
|
||||||
));
|
));
|
||||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||||
@@ -1245,10 +1246,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_manual_search_submit() {
|
fn test_manual_search_submit() {
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
let mut modal = MovieDetailsModal {
|
||||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||||
..MovieDetailsModal::default()
|
..MovieDetailsModal::default()
|
||||||
});
|
};
|
||||||
|
modal
|
||||||
|
.movie_releases
|
||||||
|
.set_items(vec![RadarrRelease::default()]);
|
||||||
|
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||||
app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into());
|
app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into());
|
||||||
|
|
||||||
MovieDetailsHandler::with(
|
MovieDetailsHandler::with(
|
||||||
@@ -1485,11 +1490,22 @@ mod tests {
|
|||||||
)]
|
)]
|
||||||
active_radarr_block: ActiveRadarrBlock,
|
active_radarr_block: ActiveRadarrBlock,
|
||||||
) {
|
) {
|
||||||
|
use crate::models::radarr_models::RadarrRelease;
|
||||||
|
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
let mut modal = MovieDetailsModal {
|
||||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
movie_details: ScrollableText::with_string("Test".to_owned()),
|
||||||
..MovieDetailsModal::default()
|
..MovieDetailsModal::default()
|
||||||
});
|
};
|
||||||
|
modal
|
||||||
|
.movie_history
|
||||||
|
.set_items(vec![MovieHistoryItem::default()]);
|
||||||
|
modal.movie_cast.set_items(vec![Credit::default()]);
|
||||||
|
modal.movie_crew.set_items(vec![Credit::default()]);
|
||||||
|
modal
|
||||||
|
.movie_releases
|
||||||
|
.set_items(vec![RadarrRelease::default()]);
|
||||||
|
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||||
|
|
||||||
MovieDetailsHandler::with(
|
MovieDetailsHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.search.key,
|
&DEFAULT_KEYBINDINGS.search.key,
|
||||||
@@ -1539,10 +1555,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_sort_key() {
|
fn test_sort_key() {
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
let mut modal = MovieDetailsModal::default();
|
||||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
modal.movie_releases.set_items(release_vec());
|
||||||
..MovieDetailsModal::default()
|
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||||
});
|
|
||||||
|
|
||||||
MovieDetailsHandler::with(
|
MovieDetailsHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.sort.key,
|
&DEFAULT_KEYBINDINGS.sort.key,
|
||||||
@@ -1670,10 +1685,19 @@ mod tests {
|
|||||||
active_radarr_block: ActiveRadarrBlock,
|
active_radarr_block: ActiveRadarrBlock,
|
||||||
) {
|
) {
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
let mut modal = MovieDetailsModal {
|
||||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
movie_details: ScrollableText::with_string("Test".to_owned()),
|
||||||
..MovieDetailsModal::default()
|
..MovieDetailsModal::default()
|
||||||
});
|
};
|
||||||
|
modal
|
||||||
|
.movie_history
|
||||||
|
.set_items(vec![MovieHistoryItem::default()]);
|
||||||
|
modal.movie_cast.set_items(vec![Credit::default()]);
|
||||||
|
modal.movie_crew.set_items(vec![Credit::default()]);
|
||||||
|
modal
|
||||||
|
.movie_releases
|
||||||
|
.set_items(vec![RadarrRelease::default()]);
|
||||||
|
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||||
|
|
||||||
MovieDetailsHandler::with(
|
MovieDetailsHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.update.key,
|
&DEFAULT_KEYBINDINGS.update.key,
|
||||||
@@ -1733,10 +1757,19 @@ mod tests {
|
|||||||
active_radarr_block: ActiveRadarrBlock,
|
active_radarr_block: ActiveRadarrBlock,
|
||||||
) {
|
) {
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
let mut modal = MovieDetailsModal {
|
||||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
movie_details: ScrollableText::with_string("Test".to_owned()),
|
||||||
..MovieDetailsModal::default()
|
..MovieDetailsModal::default()
|
||||||
});
|
};
|
||||||
|
modal
|
||||||
|
.movie_history
|
||||||
|
.set_items(vec![MovieHistoryItem::default()]);
|
||||||
|
modal.movie_cast.set_items(vec![Credit::default()]);
|
||||||
|
modal.movie_crew.set_items(vec![Credit::default()]);
|
||||||
|
modal
|
||||||
|
.movie_releases
|
||||||
|
.set_items(vec![RadarrRelease::default()]);
|
||||||
|
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||||
|
|
||||||
MovieDetailsHandler::with(
|
MovieDetailsHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.refresh.key,
|
&DEFAULT_KEYBINDINGS.refresh.key,
|
||||||
@@ -1829,7 +1862,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_source() {
|
fn test_releases_sorting_options_source() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.protocol.cmp(&b.protocol);
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||||
|
|a, b| a.protocol.cmp(&b.protocol);
|
||||||
let mut expected_releases_vec = release_vec();
|
let mut expected_releases_vec = release_vec();
|
||||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||||
|
|
||||||
@@ -1843,7 +1877,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_age() {
|
fn test_releases_sorting_options_age() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.age.cmp(&b.age);
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = |a, b| a.age.cmp(&b.age);
|
||||||
let mut expected_releases_vec = release_vec();
|
let mut expected_releases_vec = release_vec();
|
||||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||||
|
|
||||||
@@ -1857,7 +1891,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_rejected() {
|
fn test_releases_sorting_options_rejected() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.rejected.cmp(&b.rejected);
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||||
|
|a, b| a.rejected.cmp(&b.rejected);
|
||||||
let mut expected_releases_vec = release_vec();
|
let mut expected_releases_vec = release_vec();
|
||||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||||
|
|
||||||
@@ -1871,7 +1906,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_title() {
|
fn test_releases_sorting_options_title() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| {
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = |a, b| {
|
||||||
a.title
|
a.title
|
||||||
.text
|
.text
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
@@ -1890,7 +1925,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_indexer() {
|
fn test_releases_sorting_options_indexer() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering =
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||||
|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase());
|
|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase());
|
||||||
let mut expected_releases_vec = release_vec();
|
let mut expected_releases_vec = release_vec();
|
||||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||||
@@ -1905,7 +1940,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_size() {
|
fn test_releases_sorting_options_size() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.size.cmp(&b.size);
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||||
|
|a, b| a.size.cmp(&b.size);
|
||||||
let mut expected_releases_vec = release_vec();
|
let mut expected_releases_vec = release_vec();
|
||||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||||
|
|
||||||
@@ -1919,7 +1955,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_peers() {
|
fn test_releases_sorting_options_peers() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| {
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = |a, b| {
|
||||||
let default_number = Number::from(i64::MAX);
|
let default_number = Number::from(i64::MAX);
|
||||||
let seeder_a = a
|
let seeder_a = a
|
||||||
.seeders
|
.seeders
|
||||||
@@ -1949,8 +1985,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_language() {
|
fn test_releases_sorting_options_language() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| {
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = |a, b| {
|
||||||
let default_language_vec = vec![Language {
|
let default_language_vec = vec![Language {
|
||||||
|
id: 1,
|
||||||
name: "_".to_owned(),
|
name: "_".to_owned(),
|
||||||
}];
|
}];
|
||||||
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||||
@@ -1971,7 +2008,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_releases_sorting_options_quality() {
|
fn test_releases_sorting_options_quality() {
|
||||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.quality.cmp(&b.quality);
|
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||||
|
|a, b| a.quality.cmp(&b.quality);
|
||||||
let mut expected_releases_vec = release_vec();
|
let mut expected_releases_vec = release_vec();
|
||||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||||
|
|
||||||
@@ -1994,15 +2032,39 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn test_movie_details_handler_is_not_ready_when_loading() {
|
fn test_movie_details_handler_is_not_ready_when_loading(
|
||||||
|
#[values(
|
||||||
|
ActiveRadarrBlock::MovieDetails,
|
||||||
|
ActiveRadarrBlock::MovieHistory,
|
||||||
|
ActiveRadarrBlock::FileInfo,
|
||||||
|
ActiveRadarrBlock::Cast,
|
||||||
|
ActiveRadarrBlock::Crew,
|
||||||
|
ActiveRadarrBlock::ManualSearch,
|
||||||
|
ActiveRadarrBlock::ManualSearch
|
||||||
|
)]
|
||||||
|
movie_details_block: ActiveRadarrBlock,
|
||||||
|
) {
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.is_loading = true;
|
app.is_loading = true;
|
||||||
|
let mut modal = MovieDetailsModal {
|
||||||
|
movie_details: ScrollableText::with_string("Test".to_owned()),
|
||||||
|
..MovieDetailsModal::default()
|
||||||
|
};
|
||||||
|
modal
|
||||||
|
.movie_history
|
||||||
|
.set_items(vec![MovieHistoryItem::default()]);
|
||||||
|
modal.movie_cast.set_items(vec![Credit::default()]);
|
||||||
|
modal.movie_crew.set_items(vec![Credit::default()]);
|
||||||
|
modal
|
||||||
|
.movie_releases
|
||||||
|
.set_items(vec![RadarrRelease::default()]);
|
||||||
|
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||||
|
|
||||||
let handler = MovieDetailsHandler::with(
|
let handler = MovieDetailsHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.esc.key,
|
&DEFAULT_KEYBINDINGS.esc.key,
|
||||||
&mut app,
|
&mut app,
|
||||||
&ActiveRadarrBlock::MovieDetails,
|
&movie_details_block,
|
||||||
&None,
|
&None,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2105,7 +2167,9 @@ mod tests {
|
|||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.is_loading = false;
|
app.is_loading = false;
|
||||||
let mut modal = MovieDetailsModal::default();
|
let mut modal = MovieDetailsModal::default();
|
||||||
modal.movie_releases.set_items(vec![Release::default()]);
|
modal
|
||||||
|
.movie_releases
|
||||||
|
.set_items(vec![RadarrRelease::default()]);
|
||||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||||
|
|
||||||
let handler = MovieDetailsHandler::with(
|
let handler = MovieDetailsHandler::with(
|
||||||
@@ -2118,8 +2182,8 @@ mod tests {
|
|||||||
assert!(handler.is_ready());
|
assert!(handler.is_ready());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn release_vec() -> Vec<Release> {
|
fn release_vec() -> Vec<RadarrRelease> {
|
||||||
let release_a = Release {
|
let release_a = RadarrRelease {
|
||||||
protocol: "Protocol A".to_owned(),
|
protocol: "Protocol A".to_owned(),
|
||||||
age: 1,
|
age: 1,
|
||||||
title: HorizontallyScrollableText::from("Title A"),
|
title: HorizontallyScrollableText::from("Title A"),
|
||||||
@@ -2128,6 +2192,7 @@ mod tests {
|
|||||||
rejected: true,
|
rejected: true,
|
||||||
seeders: Some(Number::from(1)),
|
seeders: Some(Number::from(1)),
|
||||||
languages: Some(vec![Language {
|
languages: Some(vec![Language {
|
||||||
|
id: 1,
|
||||||
name: "Language A".to_owned(),
|
name: "Language A".to_owned(),
|
||||||
}]),
|
}]),
|
||||||
quality: QualityWrapper {
|
quality: QualityWrapper {
|
||||||
@@ -2135,9 +2200,9 @@ mod tests {
|
|||||||
name: "Quality A".to_owned(),
|
name: "Quality A".to_owned(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
..Release::default()
|
..RadarrRelease::default()
|
||||||
};
|
};
|
||||||
let release_b = Release {
|
let release_b = RadarrRelease {
|
||||||
protocol: "Protocol B".to_owned(),
|
protocol: "Protocol B".to_owned(),
|
||||||
age: 2,
|
age: 2,
|
||||||
title: HorizontallyScrollableText::from("title B"),
|
title: HorizontallyScrollableText::from("title B"),
|
||||||
@@ -2146,6 +2211,7 @@ mod tests {
|
|||||||
rejected: false,
|
rejected: false,
|
||||||
seeders: Some(Number::from(2)),
|
seeders: Some(Number::from(2)),
|
||||||
languages: Some(vec![Language {
|
languages: Some(vec![Language {
|
||||||
|
id: 2,
|
||||||
name: "Language B".to_owned(),
|
name: "Language B".to_owned(),
|
||||||
}]),
|
}]),
|
||||||
quality: QualityWrapper {
|
quality: QualityWrapper {
|
||||||
@@ -2153,9 +2219,9 @@ mod tests {
|
|||||||
name: "Quality B".to_owned(),
|
name: "Quality B".to_owned(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
..Release::default()
|
..RadarrRelease::default()
|
||||||
};
|
};
|
||||||
let release_c = Release {
|
let release_c = RadarrRelease {
|
||||||
protocol: "Protocol C".to_owned(),
|
protocol: "Protocol C".to_owned(),
|
||||||
age: 3,
|
age: 3,
|
||||||
title: HorizontallyScrollableText::from("Title C"),
|
title: HorizontallyScrollableText::from("Title C"),
|
||||||
@@ -2169,13 +2235,13 @@ mod tests {
|
|||||||
name: "Quality C".to_owned(),
|
name: "Quality C".to_owned(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
..Release::default()
|
..RadarrRelease::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
vec![release_a, release_b, release_c]
|
vec![release_a, release_b, release_c]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_options() -> Vec<SortOption<Release>> {
|
fn sort_options() -> Vec<SortOption<RadarrRelease>> {
|
||||||
vec![SortOption {
|
vec![SortOption {
|
||||||
name: "Test 1",
|
name: "Test 1",
|
||||||
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
|
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
|
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::radarr_models::RootFolder;
|
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS};
|
||||||
|
use crate::models::servarr_models::RootFolder;
|
||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
|
|
||||||
mod test_handle_scroll_up_and_down {
|
mod test_handle_scroll_up_and_down {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::models::radarr_models::RootFolder;
|
use crate::models::servarr_models::RootFolder;
|
||||||
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
|
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -63,7 +63,7 @@ mod tests {
|
|||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use crate::models::radarr_models::RootFolder;
|
use crate::models::servarr_models::RootFolder;
|
||||||
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
|
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::radarr_models::{QueueEvent, Task};
|
use crate::models::radarr_models::RadarrTask;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS,
|
ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_models::QueueEvent;
|
||||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||||
|
|
||||||
mod test_handle_scroll_up_and_down {
|
mod test_handle_scroll_up_and_down {
|
||||||
@@ -73,7 +74,7 @@ mod tests {
|
|||||||
.data
|
.data
|
||||||
.radarr_data
|
.radarr_data
|
||||||
.tasks
|
.tasks
|
||||||
.set_items(simple_stateful_iterable_vec!(Task, String, name));
|
.set_items(simple_stateful_iterable_vec!(RadarrTask, String, name));
|
||||||
|
|
||||||
SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle();
|
SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle();
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ mod tests {
|
|||||||
.data
|
.data
|
||||||
.radarr_data
|
.radarr_data
|
||||||
.tasks
|
.tasks
|
||||||
.set_items(simple_stateful_iterable_vec!(Task, String, name));
|
.set_items(simple_stateful_iterable_vec!(RadarrTask, String, name));
|
||||||
|
|
||||||
SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle();
|
SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle();
|
||||||
|
|
||||||
@@ -317,7 +318,7 @@ mod tests {
|
|||||||
.data
|
.data
|
||||||
.radarr_data
|
.radarr_data
|
||||||
.tasks
|
.tasks
|
||||||
.set_items(extended_stateful_iterable_vec!(Task, String, name));
|
.set_items(extended_stateful_iterable_vec!(RadarrTask, String, name));
|
||||||
|
|
||||||
SystemDetailsHandler::with(
|
SystemDetailsHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.end.key,
|
&DEFAULT_KEYBINDINGS.end.key,
|
||||||
@@ -356,7 +357,7 @@ mod tests {
|
|||||||
.data
|
.data
|
||||||
.radarr_data
|
.radarr_data
|
||||||
.tasks
|
.tasks
|
||||||
.set_items(extended_stateful_iterable_vec!(Task, String, name));
|
.set_items(extended_stateful_iterable_vec!(RadarrTask, String, name));
|
||||||
|
|
||||||
SystemDetailsHandler::with(
|
SystemDetailsHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.end.key,
|
&DEFAULT_KEYBINDINGS.end.key,
|
||||||
@@ -788,7 +789,11 @@ mod tests {
|
|||||||
app.is_loading = is_ready;
|
app.is_loading = is_ready;
|
||||||
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
||||||
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
|
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemTasks, &None)
|
SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemTasks, &None)
|
||||||
.handle();
|
.handle();
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::radarr_handlers::system::SystemHandler;
|
use crate::handlers::radarr_handlers::system::SystemHandler;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::models::radarr_models::{QueueEvent, Task};
|
use crate::models::radarr_models::RadarrTask;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS,
|
ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_models::QueueEvent;
|
||||||
use crate::test_handler_delegation;
|
use crate::test_handler_delegation;
|
||||||
|
|
||||||
mod test_handle_left_right_action {
|
mod test_handle_left_right_action {
|
||||||
@@ -104,7 +105,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.update.key,
|
&DEFAULT_KEYBINDINGS.update.key,
|
||||||
@@ -134,7 +139,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.update.key,
|
&DEFAULT_KEYBINDINGS.update.key,
|
||||||
@@ -159,7 +168,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.events.key,
|
&DEFAULT_KEYBINDINGS.events.key,
|
||||||
@@ -189,7 +202,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.events.key,
|
&DEFAULT_KEYBINDINGS.events.key,
|
||||||
@@ -214,7 +231,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
@@ -243,7 +264,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
@@ -270,7 +295,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.logs.key,
|
&DEFAULT_KEYBINDINGS.logs.key,
|
||||||
@@ -308,7 +337,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.logs.key,
|
&DEFAULT_KEYBINDINGS.logs.key,
|
||||||
@@ -334,7 +367,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.tasks.key,
|
&DEFAULT_KEYBINDINGS.tasks.key,
|
||||||
@@ -364,7 +401,11 @@ mod tests {
|
|||||||
.radarr_data
|
.radarr_data
|
||||||
.queued_events
|
.queued_events
|
||||||
.set_items(vec![QueueEvent::default()]);
|
.set_items(vec![QueueEvent::default()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
SystemHandler::with(
|
SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.tasks.key,
|
&DEFAULT_KEYBINDINGS.tasks.key,
|
||||||
@@ -429,7 +470,11 @@ mod tests {
|
|||||||
fn test_system_handler_is_not_ready_when_logs_is_empty() {
|
fn test_system_handler_is_not_ready_when_logs_is_empty() {
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.is_loading = false;
|
app.is_loading = false;
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
app
|
app
|
||||||
.data
|
.data
|
||||||
.radarr_data
|
.radarr_data
|
||||||
@@ -472,7 +517,11 @@ mod tests {
|
|||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.is_loading = false;
|
app.is_loading = false;
|
||||||
app.data.radarr_data.logs.set_items(vec!["test".into()]);
|
app.data.radarr_data.logs.set_items(vec!["test".into()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
|
|
||||||
let system_handler = SystemHandler::with(
|
let system_handler = SystemHandler::with(
|
||||||
&DEFAULT_KEYBINDINGS.update.key,
|
&DEFAULT_KEYBINDINGS.update.key,
|
||||||
@@ -489,7 +538,11 @@ mod tests {
|
|||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.is_loading = false;
|
app.is_loading = false;
|
||||||
app.data.radarr_data.logs.set_items(vec!["test".into()]);
|
app.data.radarr_data.logs.set_items(vec!["test".into()]);
|
||||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
app
|
||||||
|
.data
|
||||||
|
.radarr_data
|
||||||
|
.tasks
|
||||||
|
.set_items(vec![RadarrTask::default()]);
|
||||||
app
|
app
|
||||||
.data
|
.data
|
||||||
.radarr_data
|
.radarr_data
|
||||||
|
|||||||
+40
-83
@@ -1,33 +1,30 @@
|
|||||||
#![warn(rust_2018_idioms)]
|
#![warn(rust_2018_idioms)]
|
||||||
|
|
||||||
use std::fs::{self, File};
|
use anyhow::Result;
|
||||||
use std::io::BufReader;
|
|
||||||
use std::panic::PanicHookInfo;
|
use std::panic::PanicHookInfo;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{io, panic, process};
|
use std::{io, panic, process};
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser};
|
||||||
use anyhow::Result;
|
|
||||||
use app::AppConfig;
|
|
||||||
use clap::{
|
|
||||||
command, crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser,
|
|
||||||
};
|
|
||||||
use clap_complete::generate;
|
use clap_complete::generate;
|
||||||
use colored::Colorize;
|
|
||||||
use 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, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
};
|
};
|
||||||
use log::error;
|
use log::{error, warn};
|
||||||
use network::NetworkTrait;
|
use network::NetworkTrait;
|
||||||
use ratatui::backend::CrosstermBackend;
|
use ratatui::backend::CrosstermBackend;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use reqwest::{Certificate, Client};
|
use reqwest::Client;
|
||||||
|
use tokio::select;
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use utils::{
|
||||||
|
build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::cli::Command;
|
use crate::cli::Command;
|
||||||
@@ -64,6 +61,13 @@ mod utils;
|
|||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Command>,
|
command: Option<Command>,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
env = "MANAGARR_DISABLE_SPINNER",
|
||||||
|
help = "Disable the spinner (can sometimes make parsing output challenging)"
|
||||||
|
)]
|
||||||
|
disable_spinner: bool,
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
global = true,
|
global = true,
|
||||||
@@ -88,6 +92,8 @@ async fn main() -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
confy::load("managarr", "config")?
|
confy::load("managarr", "config")?
|
||||||
};
|
};
|
||||||
|
let spinner_disabled = args.disable_spinner;
|
||||||
|
config.validate();
|
||||||
let reqwest_client = build_network_client(&config);
|
let reqwest_client = build_network_client(&config);
|
||||||
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
|
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
|
||||||
let cancellation_token = CancellationToken::new();
|
let cancellation_token = CancellationToken::new();
|
||||||
@@ -102,25 +108,24 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let app = Arc::new(Mutex::new(App::new(
|
let app = Arc::new(Mutex::new(App::new(
|
||||||
sync_network_tx,
|
sync_network_tx,
|
||||||
config,
|
config.clone(),
|
||||||
cancellation_token.clone(),
|
cancellation_token.clone(),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(command) => match command {
|
Some(command) => match command {
|
||||||
Command::Radarr(_) => {
|
Command::Radarr(_) | Command::Sonarr(_) => {
|
||||||
let app_nw = Arc::clone(&app);
|
if spinner_disabled {
|
||||||
let mut network = Network::new(&app_nw, cancellation_token, reqwest_client);
|
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||||
|
} else {
|
||||||
if let Err(e) = cli::handle_command(&app, command, &mut network).await {
|
start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||||
eprintln!("error: {}", e.to_string().red());
|
|
||||||
process::exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Completions { shell } => {
|
Command::Completions { shell } => {
|
||||||
let mut cli = Cli::command();
|
let mut cli = Cli::command();
|
||||||
generate(shell, &mut cli, "managarr", &mut io::stdout())
|
generate(shell, &mut cli, "managarr", &mut io::stdout())
|
||||||
}
|
}
|
||||||
|
Command::TailLogs { no_color } => tail_logs(no_color).await,
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
let app_nw = Arc::clone(&app);
|
let app_nw = Arc::clone(&app);
|
||||||
@@ -143,9 +148,20 @@ async fn start_networking(
|
|||||||
) {
|
) {
|
||||||
let mut network = Network::new(app, cancellation_token, client);
|
let mut network = Network::new(app, cancellation_token, client);
|
||||||
|
|
||||||
while let Some(network_event) = network_rx.recv().await {
|
loop {
|
||||||
if let Err(e) = network.handle_network_event(network_event).await {
|
select! {
|
||||||
error!("Encountered an error handling network event: {e:?}");
|
Some(network_event) = network_rx.recv() => {
|
||||||
|
if let Err(e) = network.handle_network_event(network_event).await {
|
||||||
|
error!("Encountered an error handling network event: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = network.cancellation_token.cancelled() => {
|
||||||
|
warn!("Clearing network channel");
|
||||||
|
while network_rx.try_recv().is_ok() {
|
||||||
|
// Discard the message
|
||||||
|
}
|
||||||
|
network.reset_cancellation_token().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,70 +235,11 @@ fn panic_hook(info: &PanicHookInfo<'_>) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_config(path: &str) -> Result<AppConfig> {
|
|
||||||
let file = File::open(path).map_err(|e| anyhow!(e))?;
|
|
||||||
let reader = BufReader::new(file);
|
|
||||||
let config = serde_yaml::from_reader(reader)?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_network_client(config: &AppConfig) -> Client {
|
|
||||||
let mut client_builder = Client::builder();
|
|
||||||
|
|
||||||
if config.radarr.use_ssl {
|
|
||||||
let cert = create_cert(config.radarr.ssl_cert_path.clone(), "Radarr");
|
|
||||||
client_builder = client_builder.add_root_certificate(cert);
|
|
||||||
}
|
|
||||||
|
|
||||||
match client_builder.build() {
|
|
||||||
Ok(client) => client,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Unable to create reqwest client: {}", e);
|
|
||||||
eprintln!("error: {}", e.to_string().red());
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_cert(cert_path: Option<String>, servarr_name: &str) -> Certificate {
|
|
||||||
let err = |error: String| {
|
|
||||||
error!("{}", error);
|
|
||||||
eprintln!("error: {}", error.red());
|
|
||||||
process::exit(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
if cert_path.is_none() {
|
|
||||||
err(format!(
|
|
||||||
"A {} cert path is required when 'use_ssl' is 'true'",
|
|
||||||
servarr_name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
match fs::read(cert_path.unwrap()) {
|
|
||||||
Ok(cert) => match Certificate::from_pem(&cert) {
|
|
||||||
Ok(certificate) => certificate,
|
|
||||||
Err(_) => err(format!(
|
|
||||||
"Unable to read the specified {} SSL certificate",
|
|
||||||
servarr_name
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
Err(_) => err(format!(
|
|
||||||
"Unable to open specified {} SSL certificate",
|
|
||||||
servarr_name
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
fn panic_hook(info: &PanicHookInfo<'_>) {
|
fn panic_hook(info: &PanicHookInfo<'_>) {
|
||||||
use human_panic::{handle_dump, print_msg, Metadata};
|
use human_panic::{handle_dump, metadata, print_msg};
|
||||||
|
|
||||||
let meta = Metadata {
|
let meta = metadata!();
|
||||||
version: env!("CARGO_PKG_VERSION").into(),
|
|
||||||
name: env!("CARGO_PKG_NAME").into(),
|
|
||||||
authors: env!("CARGO_PKG_AUTHORS").replace(":", ", ").into(),
|
|
||||||
homepage: env!("CARGO_PKG_HOMEPAGE").into(),
|
|
||||||
};
|
|
||||||
let file_path = handle_dump(&meta, info);
|
let file_path = handle_dump(&meta, info);
|
||||||
disable_raw_mode().unwrap();
|
disable_raw_mode().unwrap();
|
||||||
execute!(io::stdout(), LeaveAlternateScreen).unwrap();
|
execute!(io::stdout(), LeaveAlternateScreen).unwrap();
|
||||||
|
|||||||
+21
-1
@@ -6,10 +6,15 @@ 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;
|
||||||
|
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||||
|
use sonarr_models::SonarrSerdeable;
|
||||||
pub mod radarr_models;
|
pub mod radarr_models;
|
||||||
pub mod servarr_data;
|
pub mod servarr_data;
|
||||||
|
pub mod servarr_models;
|
||||||
|
pub mod sonarr_models;
|
||||||
pub mod stateful_list;
|
pub mod stateful_list;
|
||||||
pub mod stateful_table;
|
pub mod stateful_table;
|
||||||
|
pub mod stateful_tree;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "model_tests.rs"]
|
#[path = "model_tests.rs"]
|
||||||
@@ -20,7 +25,7 @@ mod model_tests;
|
|||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub enum Route {
|
pub enum Route {
|
||||||
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
|
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
|
||||||
Sonarr,
|
Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
|
||||||
Readarr,
|
Readarr,
|
||||||
Lidarr,
|
Lidarr,
|
||||||
Whisparr,
|
Whisparr,
|
||||||
@@ -33,6 +38,11 @@ pub enum Route {
|
|||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum Serdeable {
|
pub enum Serdeable {
|
||||||
Radarr(RadarrSerdeable),
|
Radarr(RadarrSerdeable),
|
||||||
|
Sonarr(SonarrSerdeable),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait EnumDisplayStyle<'a> {
|
||||||
|
fn to_display_str(self) -> &'a str;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Scrollable {
|
pub trait Scrollable {
|
||||||
@@ -359,6 +369,16 @@ where
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let num: Number = Deserialize::deserialize(deserializer)?;
|
||||||
|
num.as_f64().ok_or(de::Error::custom(format!(
|
||||||
|
"Unable to convert Number to f64: {num:?}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn strip_non_search_characters(input: &str) -> String {
|
pub fn strip_non_search_characters(input: &str) -> String {
|
||||||
Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]")
|
Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ mod tests {
|
|||||||
use serde::de::IntoDeserializer;
|
use serde::de::IntoDeserializer;
|
||||||
use serde_json::to_string;
|
use serde_json::to_string;
|
||||||
|
|
||||||
|
use crate::models::from_f64;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::models::{from_i64, strip_non_search_characters};
|
use crate::models::{from_i64, strip_non_search_characters};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
@@ -649,6 +650,13 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_f64() {
|
||||||
|
let deserializer: F64Deserializer<ValueError> = 1f64.into_deserializer();
|
||||||
|
|
||||||
|
assert_eq!(from_f64(deserializer), Ok(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_horizontally_scrollable_serialize() {
|
fn test_horizontally_scrollable_serialize() {
|
||||||
let text = HorizontallyScrollableText::from("Test");
|
let text = HorizontallyScrollableText::from("Test");
|
||||||
|
|||||||
+29
-279
@@ -9,7 +9,11 @@ use strum_macros::EnumIter;
|
|||||||
|
|
||||||
use crate::{models::HorizontallyScrollableText, serde_enum_from};
|
use crate::{models::HorizontallyScrollableText, serde_enum_from};
|
||||||
|
|
||||||
use super::Serdeable;
|
use super::servarr_models::{
|
||||||
|
DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper,
|
||||||
|
QueueEvent, RootFolder, SecurityConfig, Tag, Update,
|
||||||
|
};
|
||||||
|
use super::{EnumDisplayStyle, Serdeable};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "radarr_models_tests.rs"]
|
#[path = "radarr_models_tests.rs"]
|
||||||
@@ -25,7 +29,7 @@ pub struct AddMovieBody {
|
|||||||
pub minimum_availability: String,
|
pub minimum_availability: String,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
pub tags: Vec<i64>,
|
pub tags: Vec<i64>,
|
||||||
pub add_options: AddOptions,
|
pub add_options: AddMovieOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||||
@@ -47,54 +51,11 @@ pub struct AddMovieSearchResult {
|
|||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AddOptions {
|
pub struct AddMovieOptions {
|
||||||
pub monitor: String,
|
pub monitor: String,
|
||||||
pub search_for_movie: bool,
|
pub search_for_movie: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Debug)]
|
|
||||||
pub struct AddRootFolderBody {
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, ValueEnum)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum AuthenticationMethod {
|
|
||||||
#[default]
|
|
||||||
Basic,
|
|
||||||
Forms,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AuthenticationMethod {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let authentication_method = match self {
|
|
||||||
AuthenticationMethod::Basic => "basic",
|
|
||||||
AuthenticationMethod::Forms => "forms",
|
|
||||||
AuthenticationMethod::None => "none",
|
|
||||||
};
|
|
||||||
write!(f, "{authentication_method}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, ValueEnum)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum AuthenticationRequired {
|
|
||||||
Enabled,
|
|
||||||
#[default]
|
|
||||||
DisabledForLocalAddresses,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AuthenticationRequired {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let authentication_required = match self {
|
|
||||||
AuthenticationRequired::Enabled => "enabled",
|
|
||||||
AuthenticationRequired::DisabledForLocalAddresses => "disabledForLocalAddresses",
|
|
||||||
};
|
|
||||||
write!(f, "{authentication_required}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, 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>,
|
||||||
@@ -123,26 +84,6 @@ pub struct BlocklistItemMovie {
|
|||||||
pub title: HorizontallyScrollableText,
|
pub title: HorizontallyScrollableText,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, ValueEnum)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum CertificateValidation {
|
|
||||||
#[default]
|
|
||||||
Enabled,
|
|
||||||
DisabledForLocalAddresses,
|
|
||||||
Disabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CertificateValidation {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let certificate_validation = match self {
|
|
||||||
CertificateValidation::Enabled => "enabled",
|
|
||||||
CertificateValidation::DisabledForLocalAddresses => "disabledForLocalAddresses",
|
|
||||||
CertificateValidation::Disabled => "disabled",
|
|
||||||
};
|
|
||||||
write!(f, "{certificate_validation}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, 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 {
|
||||||
@@ -175,12 +116,6 @@ pub struct CollectionMovie {
|
|||||||
pub ratings: RatingsList,
|
pub ratings: RatingsList,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CommandBody {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, 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 {
|
||||||
@@ -208,15 +143,6 @@ pub struct DeleteMovieParams {
|
|||||||
pub add_list_exclusion: bool,
|
pub add_list_exclusion: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct DiskSpace {
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub free_space: i64,
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub total_space: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Derivative, Serialize, 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 {
|
||||||
@@ -253,22 +179,6 @@ pub struct EditCollectionParams {
|
|||||||
pub search_on_add: Option<bool>,
|
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)]
|
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EditMovieParams {
|
pub struct EditMovieParams {
|
||||||
@@ -281,51 +191,6 @@ pub struct EditMovieParams {
|
|||||||
pub clear_tags: bool,
|
pub clear_tags: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct HostConfig {
|
|
||||||
pub bind_address: HorizontallyScrollableText,
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub port: i64,
|
|
||||||
pub url_base: Option<HorizontallyScrollableText>,
|
|
||||||
pub instance_name: Option<HorizontallyScrollableText>,
|
|
||||||
pub application_url: Option<HorizontallyScrollableText>,
|
|
||||||
pub enable_ssl: bool,
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub ssl_port: i64,
|
|
||||||
pub ssl_cert_path: Option<String>,
|
|
||||||
pub ssl_cert_password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Indexer {
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub id: i64,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub implementation: Option<String>,
|
|
||||||
pub implementation_name: Option<String>,
|
|
||||||
pub config_contract: Option<String>,
|
|
||||||
pub supports_rss: bool,
|
|
||||||
pub supports_search: bool,
|
|
||||||
pub fields: Option<Vec<IndexerField>>,
|
|
||||||
pub enable_rss: bool,
|
|
||||||
pub enable_automatic_search: bool,
|
|
||||||
pub enable_interactive_search: bool,
|
|
||||||
pub protocol: String,
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub priority: i64,
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub download_client_id: i64,
|
|
||||||
pub tags: Vec<Number>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
|
||||||
pub struct IndexerField {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub value: Option<Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 IndexerSettings {
|
pub struct IndexerSettings {
|
||||||
@@ -364,28 +229,6 @@ pub struct IndexerValidationFailure {
|
|||||||
pub severity: String,
|
pub severity: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
|
||||||
pub struct Language {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Log {
|
|
||||||
pub time: DateTime<Utc>,
|
|
||||||
pub exception: Option<String>,
|
|
||||||
pub exception_type: Option<String>,
|
|
||||||
pub level: String,
|
|
||||||
pub logger: Option<String>,
|
|
||||||
pub message: Option<String>,
|
|
||||||
pub method: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
|
||||||
pub struct LogResponse {
|
|
||||||
pub records: Vec<Log>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, 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")]
|
||||||
@@ -434,8 +277,8 @@ impl Display for MinimumAvailability {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MinimumAvailability {
|
impl<'a> EnumDisplayStyle<'a> for MinimumAvailability {
|
||||||
pub fn to_display_str<'a>(self) -> &'a str {
|
fn to_display_str(self) -> &'a str {
|
||||||
match self {
|
match self {
|
||||||
MinimumAvailability::Tba => "TBA",
|
MinimumAvailability::Tba => "TBA",
|
||||||
MinimumAvailability::Announced => "Announced",
|
MinimumAvailability::Announced => "Announced",
|
||||||
@@ -446,30 +289,30 @@ impl MinimumAvailability {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum)]
|
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum)]
|
||||||
pub enum Monitor {
|
pub enum MovieMonitor {
|
||||||
#[default]
|
#[default]
|
||||||
MovieOnly,
|
MovieOnly,
|
||||||
MovieAndCollection,
|
MovieAndCollection,
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Monitor {
|
impl Display for MovieMonitor {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
let monitor = match self {
|
let monitor = match self {
|
||||||
Monitor::MovieOnly => "movieOnly",
|
MovieMonitor::MovieOnly => "movieOnly",
|
||||||
Monitor::MovieAndCollection => "movieAndCollection",
|
MovieMonitor::MovieAndCollection => "movieAndCollection",
|
||||||
Monitor::None => "none",
|
MovieMonitor::None => "none",
|
||||||
};
|
};
|
||||||
write!(f, "{monitor}")
|
write!(f, "{monitor}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Monitor {
|
impl<'a> EnumDisplayStyle<'a> for MovieMonitor {
|
||||||
pub fn to_display_str<'a>(self) -> &'a str {
|
fn to_display_str(self) -> &'a str {
|
||||||
match self {
|
match self {
|
||||||
Monitor::MovieOnly => "Movie only",
|
MovieMonitor::MovieOnly => "Movie only",
|
||||||
Monitor::MovieAndCollection => "Movie and Collection",
|
MovieMonitor::MovieAndCollection => "Movie and Collection",
|
||||||
Monitor::None => "None",
|
MovieMonitor::None => "None",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,45 +381,6 @@ pub struct MovieHistoryItem {
|
|||||||
pub event_type: String,
|
pub event_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
|
||||||
pub struct Quality {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
|
||||||
pub struct QualityProfile {
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub id: i64,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(&i64, &String)> for QualityProfile {
|
|
||||||
fn from(value: (&i64, &String)) -> Self {
|
|
||||||
QualityProfile {
|
|
||||||
id: *value.0,
|
|
||||||
name: value.1.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
|
||||||
pub struct QualityWrapper {
|
|
||||||
pub quality: Quality,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct QueueEvent {
|
|
||||||
pub trigger: String,
|
|
||||||
pub name: String,
|
|
||||||
pub command_name: String,
|
|
||||||
pub status: String,
|
|
||||||
pub queued: DateTime<Utc>,
|
|
||||||
pub started: Option<DateTime<Utc>>,
|
|
||||||
pub ended: Option<DateTime<Utc>>,
|
|
||||||
pub duration: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[derivative(Default)]
|
#[derivative(Default)]
|
||||||
pub struct Rating {
|
pub struct Rating {
|
||||||
@@ -595,7 +399,7 @@ pub struct RatingsList {
|
|||||||
#[derive(Serialize, 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 RadarrRelease {
|
||||||
pub guid: String,
|
pub guid: String,
|
||||||
pub protocol: String,
|
pub protocol: String,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
@@ -616,35 +420,12 @@ pub struct Release {
|
|||||||
|
|
||||||
#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)]
|
#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ReleaseDownloadBody {
|
pub struct RadarrReleaseDownloadBody {
|
||||||
pub guid: String,
|
pub guid: String,
|
||||||
pub indexer_id: i64,
|
pub indexer_id: i64,
|
||||||
pub movie_id: i64,
|
pub movie_id: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct RootFolder {
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub id: i64,
|
|
||||||
pub path: String,
|
|
||||||
pub accessible: bool,
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub free_space: i64,
|
|
||||||
pub unmapped_folders: Option<Vec<UnmappedFolder>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct SecurityConfig {
|
|
||||||
pub authentication_method: AuthenticationMethod,
|
|
||||||
pub authentication_required: AuthenticationRequired,
|
|
||||||
pub username: String,
|
|
||||||
pub password: Option<String>,
|
|
||||||
pub api_key: String,
|
|
||||||
pub certificate_validation: CertificateValidation,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SystemStatus {
|
pub struct SystemStatus {
|
||||||
@@ -652,18 +433,11 @@ pub struct SystemStatus {
|
|||||||
pub start_time: DateTime<Utc>,
|
pub start_time: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Tag {
|
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
|
||||||
pub id: i64,
|
|
||||||
pub label: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, 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 RadarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub task_name: TaskName,
|
pub task_name: RadarrTaskName,
|
||||||
#[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>,
|
||||||
@@ -673,7 +447,7 @@ pub struct Task {
|
|||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)]
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub enum TaskName {
|
pub enum RadarrTaskName {
|
||||||
#[default]
|
#[default]
|
||||||
ApplicationCheckUpdate,
|
ApplicationCheckUpdate,
|
||||||
Backup,
|
Backup,
|
||||||
@@ -688,7 +462,7 @@ pub enum TaskName {
|
|||||||
RssSync,
|
RssSync,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for TaskName {
|
impl Display for RadarrTaskName {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
let task_name = serde_json::to_string(&self)
|
let task_name = serde_json::to_string(&self)
|
||||||
.expect("Unable to serialize task name")
|
.expect("Unable to serialize task name")
|
||||||
@@ -697,30 +471,6 @@ impl Display for TaskName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
|
|
||||||
pub struct UnmappedFolder {
|
|
||||||
pub name: String,
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Update {
|
|
||||||
pub version: String,
|
|
||||||
pub release_date: DateTime<Utc>,
|
|
||||||
pub installed: bool,
|
|
||||||
pub latest: bool,
|
|
||||||
pub installed_on: Option<DateTime<Utc>>,
|
|
||||||
pub changes: UpdateChanges,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct UpdateChanges {
|
|
||||||
pub new: Option<Vec<String>>,
|
|
||||||
pub fixed: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
@@ -741,12 +491,12 @@ pub enum RadarrSerdeable {
|
|||||||
Movies(Vec<Movie>),
|
Movies(Vec<Movie>),
|
||||||
QualityProfiles(Vec<QualityProfile>),
|
QualityProfiles(Vec<QualityProfile>),
|
||||||
QueueEvents(Vec<QueueEvent>),
|
QueueEvents(Vec<QueueEvent>),
|
||||||
Releases(Vec<Release>),
|
Releases(Vec<RadarrRelease>),
|
||||||
RootFolders(Vec<RootFolder>),
|
RootFolders(Vec<RootFolder>),
|
||||||
SecurityConfig(SecurityConfig),
|
SecurityConfig(SecurityConfig),
|
||||||
SystemStatus(SystemStatus),
|
SystemStatus(SystemStatus),
|
||||||
Tags(Vec<Tag>),
|
Tags(Vec<Tag>),
|
||||||
Tasks(Vec<Task>),
|
Tasks(Vec<RadarrTask>),
|
||||||
Updates(Vec<Update>),
|
Updates(Vec<Update>),
|
||||||
AddMovieSearchResults(Vec<AddMovieSearchResult>),
|
AddMovieSearchResults(Vec<AddMovieSearchResult>),
|
||||||
IndexerTestResults(Vec<IndexerTestResult>),
|
IndexerTestResults(Vec<IndexerTestResult>),
|
||||||
@@ -782,12 +532,12 @@ serde_enum_from!(
|
|||||||
Movies(Vec<Movie>),
|
Movies(Vec<Movie>),
|
||||||
QualityProfiles(Vec<QualityProfile>),
|
QualityProfiles(Vec<QualityProfile>),
|
||||||
QueueEvents(Vec<QueueEvent>),
|
QueueEvents(Vec<QueueEvent>),
|
||||||
Releases(Vec<Release>),
|
Releases(Vec<RadarrRelease>),
|
||||||
RootFolders(Vec<RootFolder>),
|
RootFolders(Vec<RootFolder>),
|
||||||
SecurityConfig(SecurityConfig),
|
SecurityConfig(SecurityConfig),
|
||||||
SystemStatus(SystemStatus),
|
SystemStatus(SystemStatus),
|
||||||
Tags(Vec<Tag>),
|
Tags(Vec<Tag>),
|
||||||
Tasks(Vec<Task>),
|
Tasks(Vec<RadarrTask>),
|
||||||
Updates(Vec<Update>),
|
Updates(Vec<Update>),
|
||||||
AddMovieSearchResults(Vec<AddMovieSearchResult>),
|
AddMovieSearchResults(Vec<AddMovieSearchResult>),
|
||||||
IndexerTestResults(Vec<IndexerTestResult>),
|
IndexerTestResults(Vec<IndexerTestResult>),
|
||||||
|
|||||||
@@ -5,45 +5,19 @@ mod tests {
|
|||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
radarr_models::{
|
radarr_models::{
|
||||||
AddMovieSearchResult, AuthenticationMethod, AuthenticationRequired, BlocklistItem,
|
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, DiskSpace,
|
||||||
BlocklistResponse, CertificateValidation, Collection, Credit, DiskSpace, DownloadRecord,
|
DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult,
|
||||||
DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
|
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, QualityProfile, RadarrRelease,
|
||||||
MinimumAvailability, Monitor, Movie, MovieHistoryItem, QualityProfile, QueueEvent,
|
RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus, Tag, Update,
|
||||||
RadarrSerdeable, Release, RootFolder, SystemStatus, Tag, Task, TaskName, Update,
|
|
||||||
},
|
},
|
||||||
Serdeable,
|
servarr_models::{HostConfig, Log, LogResponse, QueueEvent, RootFolder, SecurityConfig},
|
||||||
|
EnumDisplayStyle, Serdeable,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_authentication_method_display() {
|
|
||||||
assert_str_eq!(AuthenticationMethod::Basic.to_string(), "basic");
|
|
||||||
assert_str_eq!(AuthenticationMethod::Forms.to_string(), "forms");
|
|
||||||
assert_str_eq!(AuthenticationMethod::None.to_string(), "none");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_authentication_required_display() {
|
|
||||||
assert_str_eq!(AuthenticationRequired::Enabled.to_string(), "enabled");
|
|
||||||
assert_str_eq!(
|
|
||||||
AuthenticationRequired::DisabledForLocalAddresses.to_string(),
|
|
||||||
"disabledForLocalAddresses"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_certificate_validation_display() {
|
|
||||||
assert_str_eq!(CertificateValidation::Enabled.to_string(), "enabled");
|
|
||||||
assert_str_eq!(
|
|
||||||
CertificateValidation::DisabledForLocalAddresses.to_string(),
|
|
||||||
"disabledForLocalAddresses"
|
|
||||||
);
|
|
||||||
assert_str_eq!(CertificateValidation::Disabled.to_string(), "disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_task_name_display() {
|
fn test_task_name_display() {
|
||||||
assert_str_eq!(
|
assert_str_eq!(
|
||||||
TaskName::ApplicationCheckUpdate.to_string(),
|
RadarrTaskName::ApplicationCheckUpdate.to_string(),
|
||||||
"ApplicationCheckUpdate"
|
"ApplicationCheckUpdate"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,22 +43,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_monitor_display() {
|
fn test_monitor_display() {
|
||||||
assert_str_eq!(Monitor::MovieOnly.to_string(), "movieOnly");
|
assert_str_eq!(MovieMonitor::MovieOnly.to_string(), "movieOnly");
|
||||||
assert_str_eq!(
|
assert_str_eq!(
|
||||||
Monitor::MovieAndCollection.to_string(),
|
MovieMonitor::MovieAndCollection.to_string(),
|
||||||
"movieAndCollection"
|
"movieAndCollection"
|
||||||
);
|
);
|
||||||
assert_str_eq!(Monitor::None.to_string(), "none");
|
assert_str_eq!(MovieMonitor::None.to_string(), "none");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_monitor_to_display_str() {
|
fn test_monitor_to_display_str() {
|
||||||
assert_str_eq!(Monitor::MovieOnly.to_display_str(), "Movie only");
|
assert_str_eq!(MovieMonitor::MovieOnly.to_display_str(), "Movie only");
|
||||||
assert_str_eq!(
|
assert_str_eq!(
|
||||||
Monitor::MovieAndCollection.to_display_str(),
|
MovieMonitor::MovieAndCollection.to_display_str(),
|
||||||
"Movie and Collection"
|
"Movie and Collection"
|
||||||
);
|
);
|
||||||
assert_str_eq!(Monitor::None.to_display_str(), "None");
|
assert_str_eq!(MovieMonitor::None.to_display_str(), "None");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -205,6 +179,18 @@ mod tests {
|
|||||||
assert_eq!(radarr_serdeable, RadarrSerdeable::DiskSpaces(disk_spaces));
|
assert_eq!(radarr_serdeable, RadarrSerdeable::DiskSpaces(disk_spaces));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_radarr_serdeable_from_host_config() {
|
||||||
|
let host_config = HostConfig {
|
||||||
|
port: 1234,
|
||||||
|
..HostConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let radarr_serdeable: RadarrSerdeable = host_config.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(radarr_serdeable, RadarrSerdeable::HostConfig(host_config));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_serdeable_from_downloads_response() {
|
fn test_radarr_serdeable_from_downloads_response() {
|
||||||
let downloads_response = DownloadsResponse {
|
let downloads_response = DownloadsResponse {
|
||||||
@@ -331,9 +317,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_serdeable_from_releases() {
|
fn test_radarr_serdeable_from_releases() {
|
||||||
let releases = vec![Release {
|
let releases = vec![RadarrRelease {
|
||||||
size: 1,
|
size: 1,
|
||||||
..Release::default()
|
..RadarrRelease::default()
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let radarr_serdeable: RadarrSerdeable = releases.clone().into();
|
let radarr_serdeable: RadarrSerdeable = releases.clone().into();
|
||||||
@@ -353,6 +339,21 @@ mod tests {
|
|||||||
assert_eq!(radarr_serdeable, RadarrSerdeable::RootFolders(root_folders));
|
assert_eq!(radarr_serdeable, RadarrSerdeable::RootFolders(root_folders));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_radarr_serdeable_from_security_config() {
|
||||||
|
let security_config = SecurityConfig {
|
||||||
|
username: Some("Test".to_owned()),
|
||||||
|
..SecurityConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let radarr_serdeable: RadarrSerdeable = security_config.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
radarr_serdeable,
|
||||||
|
RadarrSerdeable::SecurityConfig(security_config)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_serdeable_from_system_status() {
|
fn test_radarr_serdeable_from_system_status() {
|
||||||
let system_status = SystemStatus {
|
let system_status = SystemStatus {
|
||||||
@@ -382,9 +383,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_serdeable_from_tasks() {
|
fn test_radarr_serdeable_from_tasks() {
|
||||||
let tasks = vec![Task {
|
let tasks = vec![RadarrTask {
|
||||||
name: "test".to_owned(),
|
name: "test".to_owned(),
|
||||||
..Task::default()
|
..RadarrTask::default()
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let radarr_serdeable: RadarrSerdeable = tasks.clone().into();
|
let radarr_serdeable: RadarrSerdeable = tasks.clone().into();
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
|
pub mod modals;
|
||||||
pub mod radarr;
|
pub mod radarr;
|
||||||
|
pub mod sonarr;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use crate::models::HorizontallyScrollableText;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, PartialEq, Eq)]
|
||||||
|
pub struct EditIndexerModal {
|
||||||
|
pub name: HorizontallyScrollableText,
|
||||||
|
pub enable_rss: Option<bool>,
|
||||||
|
pub enable_automatic_search: Option<bool>,
|
||||||
|
pub enable_interactive_search: Option<bool>,
|
||||||
|
pub url: HorizontallyScrollableText,
|
||||||
|
pub api_key: HorizontallyScrollableText,
|
||||||
|
pub seed_ratio: HorizontallyScrollableText,
|
||||||
|
pub tags: HorizontallyScrollableText,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Eq, PartialEq, Debug)]
|
||||||
|
pub struct IndexerTestResultModalItem {
|
||||||
|
pub name: String,
|
||||||
|
pub is_valid: bool,
|
||||||
|
pub validation_failures: HorizontallyScrollableText,
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
Collection, Credit, Indexer, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release,
|
Collection, Credit, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrRelease,
|
||||||
RootFolder,
|
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
||||||
|
use crate::models::servarr_models::{Indexer, RootFolder};
|
||||||
use crate::models::stateful_list::StatefulList;
|
use crate::models::stateful_list::StatefulList;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||||
@@ -22,19 +23,7 @@ pub struct MovieDetailsModal {
|
|||||||
pub movie_history: StatefulTable<MovieHistoryItem>,
|
pub movie_history: StatefulTable<MovieHistoryItem>,
|
||||||
pub movie_cast: StatefulTable<Credit>,
|
pub movie_cast: StatefulTable<Credit>,
|
||||||
pub movie_crew: StatefulTable<Credit>,
|
pub movie_crew: StatefulTable<Credit>,
|
||||||
pub movie_releases: StatefulTable<Release>,
|
pub movie_releases: StatefulTable<RadarrRelease>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq, Eq)]
|
|
||||||
pub struct EditIndexerModal {
|
|
||||||
pub name: HorizontallyScrollableText,
|
|
||||||
pub enable_rss: Option<bool>,
|
|
||||||
pub enable_automatic_search: Option<bool>,
|
|
||||||
pub enable_interactive_search: Option<bool>,
|
|
||||||
pub url: HorizontallyScrollableText,
|
|
||||||
pub api_key: HorizontallyScrollableText,
|
|
||||||
pub seed_ratio: HorizontallyScrollableText,
|
|
||||||
pub tags: HorizontallyScrollableText,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&RadarrData<'_>> for EditIndexerModal {
|
impl From<&RadarrData<'_>> for EditIndexerModal {
|
||||||
@@ -195,7 +184,7 @@ impl From<&RadarrData<'_>> for EditMovieModal {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct AddMovieModal {
|
pub struct AddMovieModal {
|
||||||
pub root_folder_list: StatefulList<RootFolder>,
|
pub root_folder_list: StatefulList<RootFolder>,
|
||||||
pub monitor_list: StatefulList<Monitor>,
|
pub monitor_list: StatefulList<MovieMonitor>,
|
||||||
pub minimum_availability_list: StatefulList<MinimumAvailability>,
|
pub minimum_availability_list: StatefulList<MinimumAvailability>,
|
||||||
pub quality_profile_list: StatefulList<String>,
|
pub quality_profile_list: StatefulList<String>,
|
||||||
pub tags: HorizontallyScrollableText,
|
pub tags: HorizontallyScrollableText,
|
||||||
@@ -206,7 +195,7 @@ impl From<&RadarrData<'_>> for AddMovieModal {
|
|||||||
let mut add_movie_modal = AddMovieModal::default();
|
let mut add_movie_modal = AddMovieModal::default();
|
||||||
add_movie_modal
|
add_movie_modal
|
||||||
.monitor_list
|
.monitor_list
|
||||||
.set_items(Vec::from_iter(Monitor::iter()));
|
.set_items(Vec::from_iter(MovieMonitor::iter()));
|
||||||
add_movie_modal
|
add_movie_modal
|
||||||
.minimum_availability_list
|
.minimum_availability_list
|
||||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||||
@@ -291,10 +280,3 @@ impl From<&RadarrData<'_>> for EditCollectionModal {
|
|||||||
edit_collection_modal
|
edit_collection_modal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Eq, PartialEq, Debug)]
|
|
||||||
pub struct IndexerTestResultModalItem {
|
|
||||||
pub name: String,
|
|
||||||
pub is_valid: bool,
|
|
||||||
pub validation_failures: HorizontallyScrollableText,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{Collection, MinimumAvailability, Movie, MovieMonitor};
|
||||||
Collection, Indexer, IndexerField, MinimumAvailability, Monitor, Movie, RootFolder,
|
|
||||||
};
|
|
||||||
use crate::models::servarr_data::radarr::modals::{
|
use crate::models::servarr_data::radarr::modals::{
|
||||||
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal,
|
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
|
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
||||||
|
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use bimap::BiMap;
|
use bimap::BiMap;
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
@@ -184,7 +183,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
add_movie_modal.monitor_list.items,
|
add_movie_modal.monitor_list.items,
|
||||||
Vec::from_iter(Monitor::iter())
|
Vec::from_iter(MovieMonitor::iter())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
add_movie_modal.minimum_availability_list.items,
|
add_movie_modal.minimum_availability_list.items,
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ use crate::app::radarr::radarr_context_clues::{
|
|||||||
SYSTEM_CONTEXT_CLUES,
|
SYSTEM_CONTEXT_CLUES,
|
||||||
};
|
};
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DiskSpace, DownloadRecord,
|
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord,
|
||||||
Indexer, IndexerSettings, Movie, QueueEvent, RootFolder, Task,
|
IndexerSettings, Movie, RadarrTask,
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem};
|
||||||
use crate::models::servarr_data::radarr::modals::{
|
use crate::models::servarr_data::radarr::modals::{
|
||||||
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
|
AddMovieModal, EditCollectionModal, EditMovieModal, MovieDetailsModal,
|
||||||
MovieDetailsModal,
|
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder};
|
||||||
use crate::models::stateful_list::StatefulList;
|
use crate::models::stateful_list::StatefulList;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
@@ -47,7 +48,7 @@ pub struct RadarrData<'a> {
|
|||||||
pub collection_movies: StatefulTable<CollectionMovie>,
|
pub collection_movies: StatefulTable<CollectionMovie>,
|
||||||
pub logs: StatefulList<HorizontallyScrollableText>,
|
pub logs: StatefulList<HorizontallyScrollableText>,
|
||||||
pub log_details: StatefulList<HorizontallyScrollableText>,
|
pub log_details: StatefulList<HorizontallyScrollableText>,
|
||||||
pub tasks: StatefulTable<Task>,
|
pub tasks: StatefulTable<RadarrTask>,
|
||||||
pub queued_events: StatefulTable<QueueEvent>,
|
pub queued_events: StatefulTable<QueueEvent>,
|
||||||
pub updates: ScrollableText,
|
pub updates: ScrollableText,
|
||||||
pub main_tabs: TabState,
|
pub main_tabs: TabState,
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ mod tests {
|
|||||||
use crate::assert_movie_info_tabs_reset;
|
use crate::assert_movie_info_tabs_reset;
|
||||||
use crate::models::BlockSelectionState;
|
use crate::models::BlockSelectionState;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_active_radarr_block_to_route() {
|
||||||
|
assert_eq!(
|
||||||
|
Route::from(ActiveRadarrBlock::AddMoviePrompt),
|
||||||
|
Route::Radarr(ActiveRadarrBlock::AddMoviePrompt, None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_tuple_to_route_with_context() {
|
fn test_from_tuple_to_route_with_context() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -60,7 +68,7 @@ mod tests {
|
|||||||
assert_eq!(radarr_data.disk_space_vec, Vec::new());
|
assert_eq!(radarr_data.disk_space_vec, Vec::new());
|
||||||
assert!(radarr_data.version.is_empty());
|
assert!(radarr_data.version.is_empty());
|
||||||
assert_eq!(radarr_data.start_time, <DateTime<Utc>>::default());
|
assert_eq!(radarr_data.start_time, <DateTime<Utc>>::default());
|
||||||
assert!(radarr_data.movies.items.is_empty());
|
assert!(radarr_data.movies.is_empty());
|
||||||
assert_eq!(radarr_data.selected_block, BlockSelectionState::default());
|
assert_eq!(radarr_data.selected_block, BlockSelectionState::default());
|
||||||
assert!(radarr_data.downloads.items.is_empty());
|
assert!(radarr_data.downloads.items.is_empty());
|
||||||
assert!(radarr_data.indexers.items.is_empty());
|
assert!(radarr_data.indexers.items.is_empty());
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod utils {
|
pub mod utils {
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
AddMovieSearchResult, CollectionMovie, Credit, MovieHistoryItem, Release,
|
AddMovieSearchResult, CollectionMovie, Credit, MovieHistoryItem, RadarrRelease,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
||||||
@@ -24,7 +24,7 @@ pub mod utils {
|
|||||||
.set_items(vec![Credit::default()]);
|
.set_items(vec![Credit::default()]);
|
||||||
movie_details_modal
|
movie_details_modal
|
||||||
.movie_releases
|
.movie_releases
|
||||||
.set_items(vec![Release::default()]);
|
.set_items(vec![RadarrRelease::default()]);
|
||||||
|
|
||||||
let mut radarr_data = RadarrData {
|
let mut radarr_data = RadarrData {
|
||||||
delete_movie_files: true,
|
delete_movie_files: true,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod modals;
|
||||||
|
pub mod sonarr_data;
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::models::{
|
||||||
|
servarr_data::modals::EditIndexerModal,
|
||||||
|
servarr_models::{Indexer, RootFolder},
|
||||||
|
sonarr_models::{Episode, Series, SeriesMonitor, SeriesType, SonarrHistoryItem, SonarrRelease},
|
||||||
|
stateful_list::StatefulList,
|
||||||
|
stateful_table::StatefulTable,
|
||||||
|
HorizontallyScrollableText, ScrollableText,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::sonarr_data::SonarrData;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "modals_tests.rs"]
|
||||||
|
mod modals_tests;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AddSeriesModal {
|
||||||
|
pub root_folder_list: StatefulList<RootFolder>,
|
||||||
|
pub monitor_list: StatefulList<SeriesMonitor>,
|
||||||
|
pub quality_profile_list: StatefulList<String>,
|
||||||
|
pub language_profile_list: StatefulList<String>,
|
||||||
|
pub series_type_list: StatefulList<SeriesType>,
|
||||||
|
pub use_season_folder: bool,
|
||||||
|
pub tags: HorizontallyScrollableText,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SonarrData> for AddSeriesModal {
|
||||||
|
fn from(sonarr_data: &SonarrData) -> AddSeriesModal {
|
||||||
|
let mut add_series_modal = AddSeriesModal {
|
||||||
|
use_season_folder: true,
|
||||||
|
..AddSeriesModal::default()
|
||||||
|
};
|
||||||
|
add_series_modal
|
||||||
|
.monitor_list
|
||||||
|
.set_items(Vec::from_iter(SeriesMonitor::iter()));
|
||||||
|
add_series_modal
|
||||||
|
.series_type_list
|
||||||
|
.set_items(Vec::from_iter(SeriesType::iter()));
|
||||||
|
let mut quality_profile_names: Vec<String> = sonarr_data
|
||||||
|
.quality_profile_map
|
||||||
|
.right_values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
quality_profile_names.sort();
|
||||||
|
add_series_modal
|
||||||
|
.quality_profile_list
|
||||||
|
.set_items(quality_profile_names);
|
||||||
|
let mut language_profile_names: Vec<String> = sonarr_data
|
||||||
|
.language_profiles_map
|
||||||
|
.right_values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
language_profile_names.sort();
|
||||||
|
add_series_modal
|
||||||
|
.language_profile_list
|
||||||
|
.set_items(language_profile_names);
|
||||||
|
add_series_modal
|
||||||
|
.root_folder_list
|
||||||
|
.set_items(sonarr_data.root_folders.items.to_vec());
|
||||||
|
|
||||||
|
add_series_modal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SonarrData> for EditIndexerModal {
|
||||||
|
fn from(sonarr_data: &SonarrData) -> EditIndexerModal {
|
||||||
|
let mut edit_indexer_modal = EditIndexerModal::default();
|
||||||
|
let Indexer {
|
||||||
|
name,
|
||||||
|
enable_rss,
|
||||||
|
enable_automatic_search,
|
||||||
|
enable_interactive_search,
|
||||||
|
tags,
|
||||||
|
fields,
|
||||||
|
..
|
||||||
|
} = sonarr_data.indexers.current_selection();
|
||||||
|
let seed_ratio_field_option = fields
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|field| field.name.as_ref().unwrap() == "seedCriteria.seedRatio");
|
||||||
|
let seed_ratio_value_option = if let Some(seed_ratio_field) = seed_ratio_field_option {
|
||||||
|
seed_ratio_field.value.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
edit_indexer_modal.name = name.clone().unwrap().into();
|
||||||
|
edit_indexer_modal.enable_rss = Some(*enable_rss);
|
||||||
|
edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search);
|
||||||
|
edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search);
|
||||||
|
edit_indexer_modal.url = fields
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|field| field.name.as_ref().unwrap() == "baseUrl")
|
||||||
|
.unwrap()
|
||||||
|
.value
|
||||||
|
.clone()
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
edit_indexer_modal.api_key = fields
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|field| field.name.as_ref().unwrap() == "apiKey")
|
||||||
|
.unwrap()
|
||||||
|
.value
|
||||||
|
.clone()
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
if seed_ratio_value_option.is_some() {
|
||||||
|
edit_indexer_modal.seed_ratio = seed_ratio_value_option
|
||||||
|
.unwrap()
|
||||||
|
.as_f64()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_indexer_modal.tags = tags
|
||||||
|
.iter()
|
||||||
|
.map(|tag_id| {
|
||||||
|
sonarr_data
|
||||||
|
.tags_map
|
||||||
|
.get_by_left(&tag_id.as_i64().unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
.into();
|
||||||
|
|
||||||
|
edit_indexer_modal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct EditSeriesModal {
|
||||||
|
pub series_type_list: StatefulList<SeriesType>,
|
||||||
|
pub quality_profile_list: StatefulList<String>,
|
||||||
|
pub language_profile_list: StatefulList<String>,
|
||||||
|
pub monitored: Option<bool>,
|
||||||
|
pub use_season_folders: Option<bool>,
|
||||||
|
pub path: HorizontallyScrollableText,
|
||||||
|
pub tags: HorizontallyScrollableText,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SonarrData> for EditSeriesModal {
|
||||||
|
fn from(sonarr_data: &SonarrData) -> EditSeriesModal {
|
||||||
|
let mut edit_series_modal = EditSeriesModal::default();
|
||||||
|
let Series {
|
||||||
|
path,
|
||||||
|
tags,
|
||||||
|
monitored,
|
||||||
|
season_folder,
|
||||||
|
series_type,
|
||||||
|
quality_profile_id,
|
||||||
|
language_profile_id,
|
||||||
|
..
|
||||||
|
} = sonarr_data.series.current_selection();
|
||||||
|
|
||||||
|
edit_series_modal
|
||||||
|
.series_type_list
|
||||||
|
.set_items(Vec::from_iter(SeriesType::iter()));
|
||||||
|
edit_series_modal.path = path.clone().into();
|
||||||
|
edit_series_modal.tags = tags
|
||||||
|
.iter()
|
||||||
|
.map(|tag_id| {
|
||||||
|
sonarr_data
|
||||||
|
.tags_map
|
||||||
|
.get_by_left(&tag_id.as_i64().unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
.into();
|
||||||
|
|
||||||
|
edit_series_modal.monitored = Some(*monitored);
|
||||||
|
edit_series_modal.use_season_folders = Some(*season_folder);
|
||||||
|
|
||||||
|
let series_type_index = edit_series_modal
|
||||||
|
.series_type_list
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.position(|st| st == series_type);
|
||||||
|
edit_series_modal
|
||||||
|
.series_type_list
|
||||||
|
.state
|
||||||
|
.select(series_type_index);
|
||||||
|
|
||||||
|
let mut quality_profile_names: Vec<String> = sonarr_data
|
||||||
|
.quality_profile_map
|
||||||
|
.right_values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
quality_profile_names.sort();
|
||||||
|
edit_series_modal
|
||||||
|
.quality_profile_list
|
||||||
|
.set_items(quality_profile_names);
|
||||||
|
let quality_profile_name = sonarr_data
|
||||||
|
.quality_profile_map
|
||||||
|
.get_by_left(quality_profile_id)
|
||||||
|
.unwrap();
|
||||||
|
let quality_profile_index = edit_series_modal
|
||||||
|
.quality_profile_list
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.position(|profile| profile == quality_profile_name);
|
||||||
|
edit_series_modal
|
||||||
|
.quality_profile_list
|
||||||
|
.state
|
||||||
|
.select(quality_profile_index);
|
||||||
|
let mut language_profile_names: Vec<String> = sonarr_data
|
||||||
|
.language_profiles_map
|
||||||
|
.right_values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
language_profile_names.sort();
|
||||||
|
edit_series_modal
|
||||||
|
.language_profile_list
|
||||||
|
.set_items(language_profile_names);
|
||||||
|
let language_profile_name = sonarr_data
|
||||||
|
.language_profiles_map
|
||||||
|
.get_by_left(language_profile_id)
|
||||||
|
.unwrap();
|
||||||
|
let language_profile_index = edit_series_modal
|
||||||
|
.language_profile_list
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.position(|profile| profile == language_profile_name);
|
||||||
|
edit_series_modal
|
||||||
|
.language_profile_list
|
||||||
|
.state
|
||||||
|
.select(language_profile_index);
|
||||||
|
|
||||||
|
edit_series_modal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct EpisodeDetailsModal {
|
||||||
|
// Temporarily allowing this, since the value is only current written and not read.
|
||||||
|
// This will be read from once I begin the UI work for Sonarr
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub episode_details: ScrollableText,
|
||||||
|
pub file_details: String,
|
||||||
|
pub audio_details: String,
|
||||||
|
pub video_details: String,
|
||||||
|
pub episode_history: StatefulTable<SonarrHistoryItem>,
|
||||||
|
pub episode_releases: StatefulTable<SonarrRelease>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SeasonDetailsModal {
|
||||||
|
pub episodes: StatefulTable<Episode>,
|
||||||
|
pub episode_details_modal: Option<EpisodeDetailsModal>,
|
||||||
|
pub season_releases: StatefulTable<SonarrRelease>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use bimap::BiMap;
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use rstest::rstest;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::models::servarr_data::sonarr::modals::EditSeriesModal;
|
||||||
|
use crate::models::servarr_models::{Indexer, IndexerField};
|
||||||
|
use crate::models::{
|
||||||
|
servarr_data::sonarr::{modals::AddSeriesModal, sonarr_data::SonarrData},
|
||||||
|
servarr_models::RootFolder,
|
||||||
|
sonarr_models::{SeriesMonitor, SeriesType},
|
||||||
|
};
|
||||||
|
use crate::models::{sonarr_models::Series, stateful_table::StatefulTable};
|
||||||
|
use serde_json::{Number, Value};
|
||||||
|
|
||||||
|
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_series_modal_from_sonarr_data() {
|
||||||
|
let root_folder = RootFolder {
|
||||||
|
id: 1,
|
||||||
|
path: "/nfs".to_owned(),
|
||||||
|
accessible: true,
|
||||||
|
free_space: 219902325555200,
|
||||||
|
unmapped_folders: None,
|
||||||
|
};
|
||||||
|
let mut sonarr_data = SonarrData {
|
||||||
|
quality_profile_map: BiMap::from_iter([
|
||||||
|
(2222, "HD - 1080p".to_owned()),
|
||||||
|
(1111, "Any".to_owned()),
|
||||||
|
]),
|
||||||
|
language_profiles_map: BiMap::from_iter([
|
||||||
|
(2222, "English".to_owned()),
|
||||||
|
(1111, "Any".to_owned()),
|
||||||
|
]),
|
||||||
|
..SonarrData::default()
|
||||||
|
};
|
||||||
|
sonarr_data
|
||||||
|
.root_folders
|
||||||
|
.set_items(vec![root_folder.clone()]);
|
||||||
|
|
||||||
|
let add_series_modal = AddSeriesModal::from(&sonarr_data);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
add_series_modal.monitor_list.items,
|
||||||
|
Vec::from_iter(SeriesMonitor::iter())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
add_series_modal.series_type_list.items,
|
||||||
|
Vec::from_iter(SeriesType::iter())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
add_series_modal.quality_profile_list.items,
|
||||||
|
vec!["Any".to_owned(), "HD - 1080p".to_owned()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
add_series_modal.language_profile_list.items,
|
||||||
|
vec!["Any".to_owned(), "English".to_owned()]
|
||||||
|
);
|
||||||
|
assert_eq!(add_series_modal.root_folder_list.items, vec![root_folder]);
|
||||||
|
assert!(add_series_modal.tags.text.is_empty());
|
||||||
|
assert!(add_series_modal.use_season_folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_edit_indexer_modal_from_sonarr_data(#[values(true, false)] seed_ratio_present: bool) {
|
||||||
|
let mut sonarr_data = SonarrData {
|
||||||
|
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
|
||||||
|
..SonarrData::default()
|
||||||
|
};
|
||||||
|
let mut fields = vec![
|
||||||
|
IndexerField {
|
||||||
|
name: Some("baseUrl".to_owned()),
|
||||||
|
value: Some(Value::String("https://test.com".to_owned())),
|
||||||
|
},
|
||||||
|
IndexerField {
|
||||||
|
name: Some("apiKey".to_owned()),
|
||||||
|
value: Some(Value::String("1234".to_owned())),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if seed_ratio_present {
|
||||||
|
fields.push(IndexerField {
|
||||||
|
name: Some("seedCriteria.seedRatio".to_owned()),
|
||||||
|
value: Some(Value::from(1.2f64)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let indexer = Indexer {
|
||||||
|
name: Some("Test".to_owned()),
|
||||||
|
enable_rss: true,
|
||||||
|
enable_automatic_search: true,
|
||||||
|
enable_interactive_search: true,
|
||||||
|
tags: vec![Number::from(1), Number::from(2)],
|
||||||
|
fields: Some(fields),
|
||||||
|
..Indexer::default()
|
||||||
|
};
|
||||||
|
sonarr_data.indexers.set_items(vec![indexer]);
|
||||||
|
|
||||||
|
let edit_indexer_modal = EditIndexerModal::from(&sonarr_data);
|
||||||
|
|
||||||
|
assert_str_eq!(edit_indexer_modal.name.text, "Test");
|
||||||
|
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
|
||||||
|
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
|
||||||
|
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
|
||||||
|
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
|
||||||
|
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
|
||||||
|
|
||||||
|
if seed_ratio_present {
|
||||||
|
assert_str_eq!(edit_indexer_modal.seed_ratio.text, "1.2");
|
||||||
|
} else {
|
||||||
|
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_indexer_modal_from_sonarr_data_seed_ratio_value_is_none() {
|
||||||
|
let mut sonarr_data = SonarrData {
|
||||||
|
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
|
||||||
|
..SonarrData::default()
|
||||||
|
};
|
||||||
|
let fields = vec![
|
||||||
|
IndexerField {
|
||||||
|
name: Some("baseUrl".to_owned()),
|
||||||
|
value: Some(Value::String("https://test.com".to_owned())),
|
||||||
|
},
|
||||||
|
IndexerField {
|
||||||
|
name: Some("apiKey".to_owned()),
|
||||||
|
value: Some(Value::String("1234".to_owned())),
|
||||||
|
},
|
||||||
|
IndexerField {
|
||||||
|
name: Some("seedCriteria.seedRatio".to_owned()),
|
||||||
|
value: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let indexer = Indexer {
|
||||||
|
name: Some("Test".to_owned()),
|
||||||
|
enable_rss: true,
|
||||||
|
enable_automatic_search: true,
|
||||||
|
enable_interactive_search: true,
|
||||||
|
tags: vec![Number::from(1), Number::from(2)],
|
||||||
|
fields: Some(fields),
|
||||||
|
..Indexer::default()
|
||||||
|
};
|
||||||
|
sonarr_data.indexers.set_items(vec![indexer]);
|
||||||
|
|
||||||
|
let edit_indexer_modal = EditIndexerModal::from(&sonarr_data);
|
||||||
|
|
||||||
|
assert_str_eq!(edit_indexer_modal.name.text, "Test");
|
||||||
|
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
|
||||||
|
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
|
||||||
|
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
|
||||||
|
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
|
||||||
|
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
|
||||||
|
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_edit_series_modal_from_sonarr_data(#[values(true, false)] test_filtered_series: bool) {
|
||||||
|
let mut sonarr_data = SonarrData {
|
||||||
|
quality_profile_map: BiMap::from_iter([
|
||||||
|
(2222, "HD - 1080p".to_owned()),
|
||||||
|
(1111, "Any".to_owned()),
|
||||||
|
]),
|
||||||
|
language_profiles_map: BiMap::from_iter([
|
||||||
|
(2222, "English".to_owned()),
|
||||||
|
(1111, "Any".to_owned()),
|
||||||
|
]),
|
||||||
|
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
|
||||||
|
series: StatefulTable::default(),
|
||||||
|
..SonarrData::default()
|
||||||
|
};
|
||||||
|
let series = Series {
|
||||||
|
path: "/nfs/seriess/Test".to_owned(),
|
||||||
|
monitored: true,
|
||||||
|
season_folder: true,
|
||||||
|
quality_profile_id: 2222,
|
||||||
|
language_profile_id: 2222,
|
||||||
|
series_type: SeriesType::Anime,
|
||||||
|
tags: vec![Number::from(1), Number::from(2)],
|
||||||
|
..Series::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if test_filtered_series {
|
||||||
|
sonarr_data.series.set_filtered_items(vec![series]);
|
||||||
|
} else {
|
||||||
|
sonarr_data.series.set_items(vec![series]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let edit_series_modal = EditSeriesModal::from(&sonarr_data);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
edit_series_modal.series_type_list.items,
|
||||||
|
Vec::from_iter(SeriesType::iter())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
edit_series_modal.series_type_list.current_selection(),
|
||||||
|
&SeriesType::Anime,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
edit_series_modal.quality_profile_list.items,
|
||||||
|
vec!["Any".to_owned(), "HD - 1080p".to_owned()]
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
edit_series_modal.quality_profile_list.current_selection(),
|
||||||
|
"HD - 1080p"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
edit_series_modal.language_profile_list.items,
|
||||||
|
vec!["Any".to_owned(), "English".to_owned()]
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
edit_series_modal.language_profile_list.current_selection(),
|
||||||
|
"English"
|
||||||
|
);
|
||||||
|
assert_str_eq!(edit_series_modal.path.text, "/nfs/seriess/Test");
|
||||||
|
assert_str_eq!(edit_series_modal.tags.text, "usenet, test");
|
||||||
|
assert_eq!(edit_series_modal.monitored, Some(true));
|
||||||
|
assert_eq!(edit_series_modal.use_season_folders, Some(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
use bimap::BiMap;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use strum::EnumIter;
|
||||||
|
|
||||||
|
use crate::models::{
|
||||||
|
servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem},
|
||||||
|
servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder},
|
||||||
|
sonarr_models::{
|
||||||
|
AddSeriesSearchResult, BlocklistItem, DownloadRecord, IndexerSettings, Season, Series,
|
||||||
|
SonarrHistoryItem, SonarrTask,
|
||||||
|
},
|
||||||
|
stateful_list::StatefulList,
|
||||||
|
stateful_table::StatefulTable,
|
||||||
|
HorizontallyScrollableText, Route, ScrollableText,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::modals::{AddSeriesModal, EditSeriesModal, SeasonDetailsModal};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "sonarr_data_tests.rs"]
|
||||||
|
mod sonarr_data_tests;
|
||||||
|
|
||||||
|
pub struct SonarrData {
|
||||||
|
pub add_list_exclusion: bool,
|
||||||
|
pub add_searched_series: Option<StatefulTable<AddSeriesSearchResult>>,
|
||||||
|
pub add_series_modal: Option<AddSeriesModal>,
|
||||||
|
pub add_series_search: Option<HorizontallyScrollableText>,
|
||||||
|
pub blocklist: StatefulTable<BlocklistItem>,
|
||||||
|
pub delete_series_files: bool,
|
||||||
|
pub downloads: StatefulTable<DownloadRecord>,
|
||||||
|
pub disk_space_vec: Vec<DiskSpace>,
|
||||||
|
pub edit_indexer_modal: Option<EditIndexerModal>,
|
||||||
|
pub edit_root_folder: Option<HorizontallyScrollableText>,
|
||||||
|
pub edit_series_modal: Option<EditSeriesModal>,
|
||||||
|
pub history: StatefulTable<SonarrHistoryItem>,
|
||||||
|
pub indexers: StatefulTable<Indexer>,
|
||||||
|
pub indexer_settings: Option<IndexerSettings>,
|
||||||
|
pub indexer_test_all_results: Option<StatefulTable<IndexerTestResultModalItem>>,
|
||||||
|
pub indexer_test_error: Option<String>,
|
||||||
|
pub language_profiles_map: BiMap<i64, String>,
|
||||||
|
pub logs: StatefulList<HorizontallyScrollableText>,
|
||||||
|
pub quality_profile_map: BiMap<i64, String>,
|
||||||
|
pub queued_events: StatefulTable<QueueEvent>,
|
||||||
|
pub root_folders: StatefulTable<RootFolder>,
|
||||||
|
pub seasons: StatefulTable<Season>,
|
||||||
|
pub season_details_modal: Option<SeasonDetailsModal>,
|
||||||
|
pub series: StatefulTable<Series>,
|
||||||
|
pub series_history: Option<StatefulTable<SonarrHistoryItem>>,
|
||||||
|
pub start_time: DateTime<Utc>,
|
||||||
|
pub tags_map: BiMap<i64, String>,
|
||||||
|
pub tasks: StatefulTable<SonarrTask>,
|
||||||
|
pub updates: ScrollableText,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SonarrData {
|
||||||
|
pub fn reset_delete_series_preferences(&mut self) {
|
||||||
|
self.delete_series_files = false;
|
||||||
|
self.add_list_exclusion = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SonarrData {
|
||||||
|
fn default() -> SonarrData {
|
||||||
|
SonarrData {
|
||||||
|
add_list_exclusion: false,
|
||||||
|
add_searched_series: None,
|
||||||
|
add_series_search: None,
|
||||||
|
add_series_modal: None,
|
||||||
|
blocklist: StatefulTable::default(),
|
||||||
|
downloads: StatefulTable::default(),
|
||||||
|
delete_series_files: false,
|
||||||
|
disk_space_vec: Vec::new(),
|
||||||
|
edit_indexer_modal: None,
|
||||||
|
edit_root_folder: None,
|
||||||
|
edit_series_modal: None,
|
||||||
|
history: StatefulTable::default(),
|
||||||
|
indexers: StatefulTable::default(),
|
||||||
|
indexer_settings: None,
|
||||||
|
indexer_test_error: None,
|
||||||
|
indexer_test_all_results: None,
|
||||||
|
language_profiles_map: BiMap::new(),
|
||||||
|
logs: StatefulList::default(),
|
||||||
|
quality_profile_map: BiMap::new(),
|
||||||
|
queued_events: StatefulTable::default(),
|
||||||
|
root_folders: StatefulTable::default(),
|
||||||
|
seasons: StatefulTable::default(),
|
||||||
|
season_details_modal: None,
|
||||||
|
series: StatefulTable::default(),
|
||||||
|
series_history: None,
|
||||||
|
start_time: DateTime::default(),
|
||||||
|
tags_map: BiMap::default(),
|
||||||
|
tasks: StatefulTable::default(),
|
||||||
|
updates: ScrollableText::default(),
|
||||||
|
version: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)]
|
||||||
|
pub enum ActiveSonarrBlock {
|
||||||
|
AddRootFolderPrompt,
|
||||||
|
AddSeriesAlreadyInLibrary,
|
||||||
|
AddSeriesConfirmPrompt,
|
||||||
|
AddSeriesEmptySearchResults,
|
||||||
|
AddSeriesPrompt,
|
||||||
|
AddSeriesSearchInput,
|
||||||
|
AddSeriesSearchResults,
|
||||||
|
AddSeriesSelectLanguageProfile,
|
||||||
|
AddSeriesSelectMonitor,
|
||||||
|
AddSeriesSelectQualityProfile,
|
||||||
|
AddSeriesSelectRootFolder,
|
||||||
|
AddSeriesSelectSeriesType,
|
||||||
|
AddSeriesTagsInput,
|
||||||
|
AddSeriesToggleUseSeasonFolder,
|
||||||
|
AllIndexerSettingsPrompt,
|
||||||
|
AutomaticallySearchEpisodePrompt,
|
||||||
|
AutomaticallySearchSeasonPrompt,
|
||||||
|
AutomaticallySearchSeriesPrompt,
|
||||||
|
Blocklist,
|
||||||
|
BlocklistClearAllItemsPrompt,
|
||||||
|
BlocklistItemDetails,
|
||||||
|
BlocklistSortPrompt,
|
||||||
|
DeleteBlocklistItemPrompt,
|
||||||
|
DeleteDownloadPrompt,
|
||||||
|
DeleteEpisodeFilePrompt,
|
||||||
|
DeleteIndexerPrompt,
|
||||||
|
DeleteRootFolderPrompt,
|
||||||
|
DeleteSeriesConfirmPrompt,
|
||||||
|
DeleteSeriesPrompt,
|
||||||
|
DeleteSeriesToggleAddListExclusion,
|
||||||
|
DeleteSeriesToggleDeleteFile,
|
||||||
|
Downloads,
|
||||||
|
EditIndexerPrompt,
|
||||||
|
EditSeriesPrompt,
|
||||||
|
EditSeriesConfirmPrompt,
|
||||||
|
EditSeriesPathInput,
|
||||||
|
EditSeriesSelectSeriesType,
|
||||||
|
EditSeriesSelectQualityProfile,
|
||||||
|
EditSeriesSelectLanguageProfile,
|
||||||
|
EditSeriesTagsInput,
|
||||||
|
EditSeriesToggleMonitored,
|
||||||
|
EditSeriesToggleSeasonFolder,
|
||||||
|
EpisodeDetails,
|
||||||
|
EpisodeFile,
|
||||||
|
EpisodeHistory,
|
||||||
|
EpisodesSortPrompt,
|
||||||
|
FilterEpisodes,
|
||||||
|
FilterEpisodesError,
|
||||||
|
FilterHistory,
|
||||||
|
FilterHistoryError,
|
||||||
|
FilterSeries,
|
||||||
|
FilterSeriesError,
|
||||||
|
FilterSeriesHistory,
|
||||||
|
FilterSeriesHistoryError,
|
||||||
|
History,
|
||||||
|
HistoryDetails,
|
||||||
|
HistorySortPrompt,
|
||||||
|
Indexers,
|
||||||
|
IndexerSettingsConfirmPrompt,
|
||||||
|
IndexerSettingsMaximumSizeInput,
|
||||||
|
IndexerSettingsMinimumAgeInput,
|
||||||
|
IndexerSettingsRetentionInput,
|
||||||
|
IndexerSettingsRssSyncIntervalInput,
|
||||||
|
ManualEpisodeSearch,
|
||||||
|
ManualEpisodeSearchConfirmPrompt,
|
||||||
|
ManualEpisodeSearchSortPrompt,
|
||||||
|
ManualSeasonSearch,
|
||||||
|
ManualSeasonSearchConfirmPrompt,
|
||||||
|
ManualSeasonSearchSortPrompt,
|
||||||
|
MarkHistoryItemAsFailedConfirmPrompt,
|
||||||
|
MarkHistoryItemAsFailedPrompt,
|
||||||
|
RootFolders,
|
||||||
|
SearchEpisodes,
|
||||||
|
SearchEpisodesError,
|
||||||
|
SearchHistory,
|
||||||
|
SearchHistoryError,
|
||||||
|
SearchSeason,
|
||||||
|
SearchSeasonError,
|
||||||
|
SearchSeries,
|
||||||
|
SearchSeriesError,
|
||||||
|
SearchSeriesHistory,
|
||||||
|
SearchSeriesHistoryError,
|
||||||
|
SeasonDetails,
|
||||||
|
SeasonHistory,
|
||||||
|
#[default]
|
||||||
|
Series,
|
||||||
|
SeriesDetails,
|
||||||
|
SeriesHistory,
|
||||||
|
SeriesHistorySortPrompt,
|
||||||
|
SeriesSortPrompt,
|
||||||
|
System,
|
||||||
|
SystemLogs,
|
||||||
|
SystemQueuedEvents,
|
||||||
|
SystemTasks,
|
||||||
|
SystemTaskStartConfirmPrompt,
|
||||||
|
SystemUpdates,
|
||||||
|
TestAllIndexers,
|
||||||
|
TestIndexer,
|
||||||
|
UpdateAllSeriesPrompt,
|
||||||
|
UpdateAndScanSeriesPrompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ActiveSonarrBlock> for Route {
|
||||||
|
fn from(active_sonarr_block: ActiveSonarrBlock) -> Route {
|
||||||
|
Route::Sonarr(active_sonarr_block, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(ActiveSonarrBlock, Option<ActiveSonarrBlock>)> for Route {
|
||||||
|
fn from(value: (ActiveSonarrBlock, Option<ActiveSonarrBlock>)) -> Route {
|
||||||
|
Route::Sonarr(value.0, value.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
mod sonarr_data_tests {
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use crate::models::{
|
||||||
|
servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData},
|
||||||
|
Route,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_active_sonarr_block_to_route() {
|
||||||
|
assert_eq!(
|
||||||
|
Route::from(ActiveSonarrBlock::SeriesSortPrompt),
|
||||||
|
Route::Sonarr(ActiveSonarrBlock::SeriesSortPrompt, None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_tuple_to_route_with_context() {
|
||||||
|
assert_eq!(
|
||||||
|
Route::from((
|
||||||
|
ActiveSonarrBlock::SeriesSortPrompt,
|
||||||
|
Some(ActiveSonarrBlock::Series)
|
||||||
|
)),
|
||||||
|
Route::Sonarr(
|
||||||
|
ActiveSonarrBlock::SeriesSortPrompt,
|
||||||
|
Some(ActiveSonarrBlock::Series),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reset_delete_series_preferences() {
|
||||||
|
let mut sonarr_data = SonarrData {
|
||||||
|
add_list_exclusion: true,
|
||||||
|
delete_series_files: true,
|
||||||
|
..SonarrData::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
sonarr_data.reset_delete_series_preferences();
|
||||||
|
|
||||||
|
assert!(!sonarr_data.delete_series_files);
|
||||||
|
assert!(!sonarr_data.add_list_exclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_data_defaults() {
|
||||||
|
let sonarr_data = SonarrData::default();
|
||||||
|
|
||||||
|
assert!(!sonarr_data.add_list_exclusion);
|
||||||
|
assert!(sonarr_data.add_searched_series.is_none());
|
||||||
|
assert!(sonarr_data.add_series_search.is_none());
|
||||||
|
assert!(sonarr_data.add_series_modal.is_none());
|
||||||
|
assert!(sonarr_data.blocklist.is_empty());
|
||||||
|
assert!(!sonarr_data.delete_series_files);
|
||||||
|
assert!(sonarr_data.downloads.is_empty());
|
||||||
|
assert!(sonarr_data.disk_space_vec.is_empty());
|
||||||
|
assert!(sonarr_data.edit_indexer_modal.is_none());
|
||||||
|
assert!(sonarr_data.edit_root_folder.is_none());
|
||||||
|
assert!(sonarr_data.edit_series_modal.is_none());
|
||||||
|
assert!(sonarr_data.history.is_empty());
|
||||||
|
assert!(sonarr_data.indexers.is_empty());
|
||||||
|
assert!(sonarr_data.indexer_settings.is_none());
|
||||||
|
assert!(sonarr_data.indexer_test_error.is_none());
|
||||||
|
assert!(sonarr_data.indexer_test_all_results.is_none());
|
||||||
|
assert!(sonarr_data.language_profiles_map.is_empty());
|
||||||
|
assert!(sonarr_data.logs.is_empty());
|
||||||
|
assert!(sonarr_data.quality_profile_map.is_empty());
|
||||||
|
assert!(sonarr_data.queued_events.is_empty());
|
||||||
|
assert!(sonarr_data.root_folders.is_empty());
|
||||||
|
assert!(sonarr_data.seasons.is_empty());
|
||||||
|
assert!(sonarr_data.season_details_modal.is_none());
|
||||||
|
assert!(sonarr_data.series.is_empty());
|
||||||
|
assert!(sonarr_data.series_history.is_none());
|
||||||
|
assert_eq!(sonarr_data.start_time, <DateTime<Utc>>::default());
|
||||||
|
assert!(sonarr_data.tags_map.is_empty());
|
||||||
|
assert!(sonarr_data.tasks.is_empty());
|
||||||
|
assert!(sonarr_data.updates.is_empty());
|
||||||
|
assert!(sonarr_data.version.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Number, Value};
|
||||||
|
|
||||||
|
use super::HorizontallyScrollableText;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "servarr_models_tests.rs"]
|
||||||
|
mod servarr_models_tests;
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Debug)]
|
||||||
|
pub struct AddRootFolderBody {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, ValueEnum)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum AuthenticationMethod {
|
||||||
|
#[default]
|
||||||
|
Basic,
|
||||||
|
Forms,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AuthenticationMethod {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
|
let authentication_method = match self {
|
||||||
|
AuthenticationMethod::Basic => "basic",
|
||||||
|
AuthenticationMethod::Forms => "forms",
|
||||||
|
AuthenticationMethod::None => "none",
|
||||||
|
};
|
||||||
|
write!(f, "{authentication_method}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, ValueEnum)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum AuthenticationRequired {
|
||||||
|
Enabled,
|
||||||
|
#[default]
|
||||||
|
DisabledForLocalAddresses,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AuthenticationRequired {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
|
let authentication_required = match self {
|
||||||
|
AuthenticationRequired::Enabled => "enabled",
|
||||||
|
AuthenticationRequired::DisabledForLocalAddresses => "disabledForLocalAddresses",
|
||||||
|
};
|
||||||
|
write!(f, "{authentication_required}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, ValueEnum)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum CertificateValidation {
|
||||||
|
#[default]
|
||||||
|
Enabled,
|
||||||
|
DisabledForLocalAddresses,
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CertificateValidation {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
|
let certificate_validation = match self {
|
||||||
|
CertificateValidation::Enabled => "enabled",
|
||||||
|
CertificateValidation::DisabledForLocalAddresses => "disabledForLocalAddresses",
|
||||||
|
CertificateValidation::Disabled => "disabled",
|
||||||
|
};
|
||||||
|
write!(f, "{certificate_validation}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CommandBody {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DiskSpace {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub free_space: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub total_space: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HostConfig {
|
||||||
|
pub bind_address: HorizontallyScrollableText,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub port: i64,
|
||||||
|
pub url_base: Option<HorizontallyScrollableText>,
|
||||||
|
pub instance_name: Option<HorizontallyScrollableText>,
|
||||||
|
pub application_url: Option<HorizontallyScrollableText>,
|
||||||
|
pub enable_ssl: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub ssl_port: i64,
|
||||||
|
pub ssl_cert_path: Option<String>,
|
||||||
|
pub ssl_cert_password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Indexer {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub implementation: Option<String>,
|
||||||
|
pub implementation_name: Option<String>,
|
||||||
|
pub config_contract: Option<String>,
|
||||||
|
pub supports_rss: bool,
|
||||||
|
pub supports_search: bool,
|
||||||
|
pub fields: Option<Vec<IndexerField>>,
|
||||||
|
pub enable_rss: bool,
|
||||||
|
pub enable_automatic_search: bool,
|
||||||
|
pub enable_interactive_search: bool,
|
||||||
|
pub protocol: String,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub priority: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub download_client_id: i64,
|
||||||
|
pub tags: Vec<Number>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct IndexerField {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub value: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||||
|
pub struct Language {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Log {
|
||||||
|
pub time: DateTime<Utc>,
|
||||||
|
pub exception: Option<String>,
|
||||||
|
pub exception_type: Option<String>,
|
||||||
|
pub level: String,
|
||||||
|
pub logger: Option<String>,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub method: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||||
|
pub struct LogResponse {
|
||||||
|
pub records: Vec<Log>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||||
|
pub struct Quality {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
|
pub struct QualityProfile {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&i64, &String)> for QualityProfile {
|
||||||
|
fn from(value: (&i64, &String)) -> Self {
|
||||||
|
QualityProfile {
|
||||||
|
id: *value.0,
|
||||||
|
name: value.1.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||||
|
pub struct QualityWrapper {
|
||||||
|
pub quality: Quality,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct QueueEvent {
|
||||||
|
pub trigger: String,
|
||||||
|
pub name: String,
|
||||||
|
pub command_name: String,
|
||||||
|
pub status: String,
|
||||||
|
pub queued: DateTime<Utc>,
|
||||||
|
pub started: Option<DateTime<Utc>>,
|
||||||
|
pub ended: Option<DateTime<Utc>>,
|
||||||
|
pub duration: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RootFolder {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
pub path: String,
|
||||||
|
pub accessible: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub free_space: i64,
|
||||||
|
pub unmapped_folders: Option<Vec<UnmappedFolder>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SecurityConfig {
|
||||||
|
pub authentication_method: AuthenticationMethod,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub authentication_required: Option<AuthenticationRequired>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub username: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub password: Option<String>,
|
||||||
|
pub api_key: String,
|
||||||
|
pub certificate_validation: CertificateValidation,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Tag {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct UnmappedFolder {
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Update {
|
||||||
|
pub version: String,
|
||||||
|
pub release_date: DateTime<Utc>,
|
||||||
|
pub installed: bool,
|
||||||
|
pub latest: bool,
|
||||||
|
pub installed_on: Option<DateTime<Utc>>,
|
||||||
|
pub changes: UpdateChanges,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateChanges {
|
||||||
|
pub new: Option<Vec<String>>,
|
||||||
|
pub fixed: Option<Vec<String>>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::models::servarr_models::{
|
||||||
|
AuthenticationMethod, AuthenticationRequired, CertificateValidation, QualityProfile,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_authentication_method_display() {
|
||||||
|
assert_str_eq!(AuthenticationMethod::Basic.to_string(), "basic");
|
||||||
|
assert_str_eq!(AuthenticationMethod::Forms.to_string(), "forms");
|
||||||
|
assert_str_eq!(AuthenticationMethod::None.to_string(), "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_authentication_required_display() {
|
||||||
|
assert_str_eq!(AuthenticationRequired::Enabled.to_string(), "enabled");
|
||||||
|
assert_str_eq!(
|
||||||
|
AuthenticationRequired::DisabledForLocalAddresses.to_string(),
|
||||||
|
"disabledForLocalAddresses"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_certificate_validation_display() {
|
||||||
|
assert_str_eq!(CertificateValidation::Enabled.to_string(), "enabled");
|
||||||
|
assert_str_eq!(
|
||||||
|
CertificateValidation::DisabledForLocalAddresses.to_string(),
|
||||||
|
"disabledForLocalAddresses"
|
||||||
|
);
|
||||||
|
assert_str_eq!(CertificateValidation::Disabled.to_string(), "disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quality_profile_from_tuple_ref() {
|
||||||
|
let id = 2;
|
||||||
|
let name = "Test".to_owned();
|
||||||
|
let quality_profile_tuple = (&id, &name);
|
||||||
|
let expected_quality_profile = QualityProfile {
|
||||||
|
id: 2,
|
||||||
|
name: "Test".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let quality_profile = QualityProfile::from(quality_profile_tuple);
|
||||||
|
|
||||||
|
assert_eq!(expected_quality_profile, quality_profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,694 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use derivative::Derivative;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{json, Number, Value};
|
||||||
|
use strum::EnumIter;
|
||||||
|
|
||||||
|
use crate::serde_enum_from;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
radarr_models::IndexerTestResult,
|
||||||
|
servarr_models::{
|
||||||
|
DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper,
|
||||||
|
QueueEvent, RootFolder, SecurityConfig, Tag, Update,
|
||||||
|
},
|
||||||
|
EnumDisplayStyle, HorizontallyScrollableText, Serdeable,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "sonarr_models_tests.rs"]
|
||||||
|
mod sonarr_models_tests;
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AddSeriesBody {
|
||||||
|
pub tvdb_id: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub monitored: bool,
|
||||||
|
pub root_folder_path: String,
|
||||||
|
pub quality_profile_id: i64,
|
||||||
|
pub language_profile_id: i64,
|
||||||
|
pub series_type: String,
|
||||||
|
pub season_folder: bool,
|
||||||
|
pub tags: Vec<i64>,
|
||||||
|
pub add_options: AddSeriesOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AddSeriesSearchResult {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub tvdb_id: i64,
|
||||||
|
pub title: HorizontallyScrollableText,
|
||||||
|
pub status: Option<String>,
|
||||||
|
pub ended: bool,
|
||||||
|
pub overview: Option<String>,
|
||||||
|
pub genres: Vec<String>,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub year: i64,
|
||||||
|
pub network: Option<String>,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub runtime: i64,
|
||||||
|
pub ratings: Option<Rating>,
|
||||||
|
pub statistics: Option<AddSeriesSearchResultStatistics>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AddSeriesSearchResultStatistics {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub season_count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AddSeriesOptions {
|
||||||
|
pub monitor: String,
|
||||||
|
pub search_for_cutoff_unmet_episodes: bool,
|
||||||
|
pub search_for_missing_episodes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BlocklistItem {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub series_id: i64,
|
||||||
|
pub episode_ids: Vec<Number>,
|
||||||
|
pub source_title: String,
|
||||||
|
pub language: Language,
|
||||||
|
pub quality: QualityWrapper,
|
||||||
|
pub date: DateTime<Utc>,
|
||||||
|
pub protocol: String,
|
||||||
|
pub indexer: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct BlocklistResponse {
|
||||||
|
pub records: Vec<BlocklistItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub struct DeleteSeriesParams {
|
||||||
|
pub id: i64,
|
||||||
|
pub delete_series_files: bool,
|
||||||
|
pub add_list_exclusion: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DownloadRecord {
|
||||||
|
pub title: String,
|
||||||
|
pub status: String,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub episode_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_f64")]
|
||||||
|
pub size: f64,
|
||||||
|
#[serde(deserialize_with = "super::from_f64")]
|
||||||
|
pub sizeleft: f64,
|
||||||
|
pub output_path: Option<HorizontallyScrollableText>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub indexer: String,
|
||||||
|
pub download_client: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for DownloadRecord {}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DownloadsResponse {
|
||||||
|
pub records: Vec<DownloadRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EditSeriesParams {
|
||||||
|
pub series_id: i64,
|
||||||
|
pub monitored: Option<bool>,
|
||||||
|
pub use_season_folders: Option<bool>,
|
||||||
|
pub quality_profile_id: Option<i64>,
|
||||||
|
pub language_profile_id: Option<i64>,
|
||||||
|
pub series_type: Option<SeriesType>,
|
||||||
|
pub root_folder_path: Option<String>,
|
||||||
|
pub tags: Option<Vec<i64>>,
|
||||||
|
pub clear_tags: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Episode {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub series_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub tvdb_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub episode_file_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub season_number: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub episode_number: i64,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub air_date_utc: Option<DateTime<Utc>>,
|
||||||
|
pub overview: Option<String>,
|
||||||
|
pub has_file: bool,
|
||||||
|
pub monitored: bool,
|
||||||
|
pub episode_file: Option<EpisodeFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Episode {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.title.as_ref().unwrap_or(&String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EpisodeFile {
|
||||||
|
pub relative_path: String,
|
||||||
|
pub path: String,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub size: i64,
|
||||||
|
pub language: Language,
|
||||||
|
pub date_added: DateTime<Utc>,
|
||||||
|
pub media_info: Option<MediaInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct IndexerSettings {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub minimum_age: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub retention: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub maximum_size: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub rss_sync_interval: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[derivative(Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MediaInfo {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub audio_bitrate: i64,
|
||||||
|
#[derivative(Default(value = "Number::from(0)"))]
|
||||||
|
pub audio_channels: Number,
|
||||||
|
pub audio_codec: Option<String>,
|
||||||
|
pub audio_languages: Option<String>,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub audio_stream_count: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub video_bit_depth: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub video_bitrate: i64,
|
||||||
|
pub video_codec: String,
|
||||||
|
#[derivative(Default(value = "Number::from(0)"))]
|
||||||
|
pub video_fps: Number,
|
||||||
|
pub resolution: String,
|
||||||
|
pub run_time: String,
|
||||||
|
pub scan_type: String,
|
||||||
|
pub subtitles: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
|
#[derivative(Default)]
|
||||||
|
pub struct Rating {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub votes: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_f64")]
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Rating {}
|
||||||
|
|
||||||
|
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Season {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub season_number: i64,
|
||||||
|
pub monitored: bool,
|
||||||
|
pub statistics: SeasonStatistics,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SeasonStatistics {
|
||||||
|
pub next_airing: Option<DateTime<Utc>>,
|
||||||
|
pub previous_airing: Option<DateTime<Utc>>,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub episode_file_count: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub episode_count: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub total_episode_count: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub size_on_disk: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_f64")]
|
||||||
|
pub percent_of_episodes: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for SeasonStatistics {}
|
||||||
|
|
||||||
|
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Series {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub tvdb_id: i64,
|
||||||
|
pub title: HorizontallyScrollableText,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub quality_profile_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub language_profile_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub runtime: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub year: i64,
|
||||||
|
pub monitored: bool,
|
||||||
|
pub series_type: SeriesType,
|
||||||
|
pub path: String,
|
||||||
|
pub genres: Vec<String>,
|
||||||
|
pub tags: Vec<Number>,
|
||||||
|
pub ratings: Rating,
|
||||||
|
pub ended: bool,
|
||||||
|
pub status: SeriesStatus,
|
||||||
|
pub overview: Option<String>,
|
||||||
|
pub network: Option<String>,
|
||||||
|
pub season_folder: bool,
|
||||||
|
pub certification: Option<String>,
|
||||||
|
pub statistics: Option<SeriesStatistics>,
|
||||||
|
pub seasons: Option<Vec<Season>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum SeriesMonitor {
|
||||||
|
#[default]
|
||||||
|
All,
|
||||||
|
Unknown,
|
||||||
|
Future,
|
||||||
|
Missing,
|
||||||
|
Existing,
|
||||||
|
FirstSeason,
|
||||||
|
LastSeason,
|
||||||
|
LatestSeason,
|
||||||
|
Pilot,
|
||||||
|
Recent,
|
||||||
|
MonitorSpecials,
|
||||||
|
UnmonitorSpecials,
|
||||||
|
None,
|
||||||
|
Skip,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SeriesMonitor {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let series_monitor = match self {
|
||||||
|
SeriesMonitor::Unknown => "unknown",
|
||||||
|
SeriesMonitor::All => "all",
|
||||||
|
SeriesMonitor::Future => "future",
|
||||||
|
SeriesMonitor::Missing => "missing",
|
||||||
|
SeriesMonitor::Existing => "existing",
|
||||||
|
SeriesMonitor::FirstSeason => "firstSeason",
|
||||||
|
SeriesMonitor::LastSeason => "lastSeason",
|
||||||
|
SeriesMonitor::LatestSeason => "latestSeason",
|
||||||
|
SeriesMonitor::Pilot => "pilot",
|
||||||
|
SeriesMonitor::Recent => "recent",
|
||||||
|
SeriesMonitor::MonitorSpecials => "monitorSpecials",
|
||||||
|
SeriesMonitor::UnmonitorSpecials => "unmonitorSpecials",
|
||||||
|
SeriesMonitor::None => "none",
|
||||||
|
SeriesMonitor::Skip => "skip",
|
||||||
|
};
|
||||||
|
write!(f, "{series_monitor}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EnumDisplayStyle<'a> for SeriesMonitor {
|
||||||
|
fn to_display_str(self) -> &'a str {
|
||||||
|
match self {
|
||||||
|
SeriesMonitor::Unknown => "Unknown",
|
||||||
|
SeriesMonitor::All => "All Episodes",
|
||||||
|
SeriesMonitor::Future => "Future Episodes",
|
||||||
|
SeriesMonitor::Missing => "Missing Episodes",
|
||||||
|
SeriesMonitor::Existing => "Existing Episodes",
|
||||||
|
SeriesMonitor::FirstSeason => "Only First Season",
|
||||||
|
SeriesMonitor::LastSeason => "Only Last Season",
|
||||||
|
SeriesMonitor::LatestSeason => "Only Latest Season",
|
||||||
|
SeriesMonitor::Pilot => "Pilot Episode",
|
||||||
|
SeriesMonitor::Recent => "Recent Episodes",
|
||||||
|
SeriesMonitor::MonitorSpecials => "Only Specials",
|
||||||
|
SeriesMonitor::UnmonitorSpecials => "Not Specials",
|
||||||
|
SeriesMonitor::None => "None",
|
||||||
|
SeriesMonitor::Skip => "Skip",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum SeriesType {
|
||||||
|
#[default]
|
||||||
|
Standard,
|
||||||
|
Daily,
|
||||||
|
Anime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SeriesType {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let series_type = match self {
|
||||||
|
SeriesType::Standard => "standard",
|
||||||
|
SeriesType::Daily => "daily",
|
||||||
|
SeriesType::Anime => "anime",
|
||||||
|
};
|
||||||
|
write!(f, "{series_type}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EnumDisplayStyle<'a> for SeriesType {
|
||||||
|
fn to_display_str(self) -> &'a str {
|
||||||
|
match self {
|
||||||
|
SeriesType::Standard => "Standard",
|
||||||
|
SeriesType::Daily => "Daily",
|
||||||
|
SeriesType::Anime => "Anime",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SeriesStatistics {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub season_count: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub episode_file_count: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub episode_count: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub total_episode_count: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub size_on_disk: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_f64")]
|
||||||
|
pub percent_of_episodes: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for SeriesStatistics {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum SeriesStatus {
|
||||||
|
#[default]
|
||||||
|
Continuing,
|
||||||
|
Ended,
|
||||||
|
Upcoming,
|
||||||
|
Deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SeriesStatus {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let series_status = match self {
|
||||||
|
SeriesStatus::Continuing => "continuing",
|
||||||
|
SeriesStatus::Ended => "ended",
|
||||||
|
SeriesStatus::Upcoming => "upcoming",
|
||||||
|
SeriesStatus::Deleted => "deleted",
|
||||||
|
};
|
||||||
|
write!(f, "{series_status}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EnumDisplayStyle<'a> for SeriesStatus {
|
||||||
|
fn to_display_str(self) -> &'a str {
|
||||||
|
match self {
|
||||||
|
SeriesStatus::Continuing => "Continuing",
|
||||||
|
SeriesStatus::Ended => "Ended",
|
||||||
|
SeriesStatus::Upcoming => "Upcoming",
|
||||||
|
SeriesStatus::Deleted => "Deleted",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SonarrHistoryWrapper {
|
||||||
|
pub records: Vec<SonarrHistoryItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SonarrHistoryData {
|
||||||
|
pub dropped_path: Option<String>,
|
||||||
|
pub imported_path: Option<String>,
|
||||||
|
pub indexer: Option<String>,
|
||||||
|
pub release_group: Option<String>,
|
||||||
|
pub series_match_type: Option<String>,
|
||||||
|
pub nzb_info_url: Option<String>,
|
||||||
|
pub download_client_name: Option<String>,
|
||||||
|
pub age: Option<String>,
|
||||||
|
pub published_date: Option<DateTime<Utc>>,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum SonarrHistoryEventType {
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
Grabbed,
|
||||||
|
SeriesFolderImported,
|
||||||
|
DownloadFolderImported,
|
||||||
|
DownloadFailed,
|
||||||
|
EpisodeFileDeleted,
|
||||||
|
EpisodeFileRenamed,
|
||||||
|
DownloadIgnored,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SonarrHistoryEventType {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let event_type = match self {
|
||||||
|
SonarrHistoryEventType::Unknown => "unknown",
|
||||||
|
SonarrHistoryEventType::Grabbed => "grabbed",
|
||||||
|
SonarrHistoryEventType::SeriesFolderImported => "seriesFolderImported",
|
||||||
|
SonarrHistoryEventType::DownloadFolderImported => "downloadFolderImported",
|
||||||
|
SonarrHistoryEventType::DownloadFailed => "downloadFailed",
|
||||||
|
SonarrHistoryEventType::EpisodeFileDeleted => "episodeFileDeleted",
|
||||||
|
SonarrHistoryEventType::EpisodeFileRenamed => "episodeFileRenamed",
|
||||||
|
SonarrHistoryEventType::DownloadIgnored => "downloadIgnored",
|
||||||
|
};
|
||||||
|
write!(f, "{event_type}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EnumDisplayStyle<'a> for SonarrHistoryEventType {
|
||||||
|
fn to_display_str(self) -> &'a str {
|
||||||
|
match self {
|
||||||
|
SonarrHistoryEventType::Unknown => "Unknown",
|
||||||
|
SonarrHistoryEventType::Grabbed => "Grabbed",
|
||||||
|
SonarrHistoryEventType::SeriesFolderImported => "Series Folder Imported",
|
||||||
|
SonarrHistoryEventType::DownloadFolderImported => "Download Folder Imported",
|
||||||
|
SonarrHistoryEventType::DownloadFailed => "Download Failed",
|
||||||
|
SonarrHistoryEventType::EpisodeFileDeleted => "Episode File Deleted",
|
||||||
|
SonarrHistoryEventType::EpisodeFileRenamed => "Episode File Renamed",
|
||||||
|
SonarrHistoryEventType::DownloadIgnored => "Download Ignored",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SonarrHistoryItem {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
|
pub source_title: HorizontallyScrollableText,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub episode_id: i64,
|
||||||
|
pub quality: QualityWrapper,
|
||||||
|
pub language: Language,
|
||||||
|
pub date: DateTime<Utc>,
|
||||||
|
pub event_type: String,
|
||||||
|
pub data: SonarrHistoryData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SonarrCommandBody {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub series_id: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub season_number: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub episode_ids: Option<Vec<i64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct SonarrRelease {
|
||||||
|
pub guid: String,
|
||||||
|
pub protocol: String,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub age: i64,
|
||||||
|
pub title: HorizontallyScrollableText,
|
||||||
|
pub indexer: String,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub indexer_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub size: i64,
|
||||||
|
pub rejected: bool,
|
||||||
|
pub rejections: Option<Vec<String>>,
|
||||||
|
pub seeders: Option<Number>,
|
||||||
|
pub leechers: Option<Number>,
|
||||||
|
pub languages: Option<Vec<Language>>,
|
||||||
|
pub quality: QualityWrapper,
|
||||||
|
pub full_season: bool,
|
||||||
|
}
|
||||||
|
#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SonarrReleaseDownloadBody {
|
||||||
|
pub guid: String,
|
||||||
|
pub indexer_id: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub series_id: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub episode_id: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub season_number: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SonarrTask {
|
||||||
|
pub name: String,
|
||||||
|
pub task_name: SonarrTaskName,
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub interval: i64,
|
||||||
|
pub last_execution: DateTime<Utc>,
|
||||||
|
pub next_execution: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub enum SonarrTaskName {
|
||||||
|
#[default]
|
||||||
|
ApplicationUpdateCheck,
|
||||||
|
Backup,
|
||||||
|
CheckHealth,
|
||||||
|
CleanUpRecycleBin,
|
||||||
|
Housekeeping,
|
||||||
|
ImportListSync,
|
||||||
|
MessagingCleanup,
|
||||||
|
RefreshMonitoredDownloads,
|
||||||
|
RefreshSeries,
|
||||||
|
RssSync,
|
||||||
|
UpdateSceneMapping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SonarrTaskName {
|
||||||
|
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(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum SonarrSerdeable {
|
||||||
|
AddSeriesSearchResults(Vec<AddSeriesSearchResult>),
|
||||||
|
BlocklistResponse(BlocklistResponse),
|
||||||
|
DownloadsResponse(DownloadsResponse),
|
||||||
|
DiskSpaces(Vec<DiskSpace>),
|
||||||
|
Episode(Episode),
|
||||||
|
Episodes(Vec<Episode>),
|
||||||
|
HostConfig(HostConfig),
|
||||||
|
IndexerSettings(IndexerSettings),
|
||||||
|
Indexers(Vec<Indexer>),
|
||||||
|
IndexerTestResults(Vec<IndexerTestResult>),
|
||||||
|
LanguageProfiles(Vec<Language>),
|
||||||
|
LogResponse(LogResponse),
|
||||||
|
QualityProfiles(Vec<QualityProfile>),
|
||||||
|
QueueEvents(Vec<QueueEvent>),
|
||||||
|
Releases(Vec<SonarrRelease>),
|
||||||
|
RootFolders(Vec<RootFolder>),
|
||||||
|
SecurityConfig(SecurityConfig),
|
||||||
|
SeriesVec(Vec<Series>),
|
||||||
|
Series(Series),
|
||||||
|
SonarrHistoryItems(Vec<SonarrHistoryItem>),
|
||||||
|
SonarrHistoryWrapper(SonarrHistoryWrapper),
|
||||||
|
SystemStatus(SystemStatus),
|
||||||
|
Tag(Tag),
|
||||||
|
Tags(Vec<Tag>),
|
||||||
|
Tasks(Vec<SonarrTask>),
|
||||||
|
Updates(Vec<Update>),
|
||||||
|
Value(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SonarrSerdeable> for Serdeable {
|
||||||
|
fn from(value: SonarrSerdeable) -> Serdeable {
|
||||||
|
Serdeable::Sonarr(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<()> for SonarrSerdeable {
|
||||||
|
fn from(_: ()) -> Self {
|
||||||
|
SonarrSerdeable::Value(json!({}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_enum_from!(
|
||||||
|
SonarrSerdeable {
|
||||||
|
AddSeriesSearchResults(Vec<AddSeriesSearchResult>),
|
||||||
|
BlocklistResponse(BlocklistResponse),
|
||||||
|
DownloadsResponse(DownloadsResponse),
|
||||||
|
DiskSpaces(Vec<DiskSpace>),
|
||||||
|
Episode(Episode),
|
||||||
|
Episodes(Vec<Episode>),
|
||||||
|
HostConfig(HostConfig),
|
||||||
|
IndexerSettings(IndexerSettings),
|
||||||
|
Indexers(Vec<Indexer>),
|
||||||
|
IndexerTestResults(Vec<IndexerTestResult>),
|
||||||
|
LanguageProfiles(Vec<Language>),
|
||||||
|
LogResponse(LogResponse),
|
||||||
|
QualityProfiles(Vec<QualityProfile>),
|
||||||
|
QueueEvents(Vec<QueueEvent>),
|
||||||
|
Releases(Vec<SonarrRelease>),
|
||||||
|
RootFolders(Vec<RootFolder>),
|
||||||
|
SecurityConfig(SecurityConfig),
|
||||||
|
SeriesVec(Vec<Series>),
|
||||||
|
Series(Series),
|
||||||
|
SonarrHistoryItems(Vec<SonarrHistoryItem>),
|
||||||
|
SonarrHistoryWrapper(SonarrHistoryWrapper),
|
||||||
|
SystemStatus(SystemStatus),
|
||||||
|
Tag(Tag),
|
||||||
|
Tags(Vec<Tag>),
|
||||||
|
Tasks(Vec<SonarrTask>),
|
||||||
|
Updates(Vec<Update>),
|
||||||
|
Value(Value),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SystemStatus {
|
||||||
|
pub version: String,
|
||||||
|
pub start_time: DateTime<Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::models::{
|
||||||
|
radarr_models::IndexerTestResult,
|
||||||
|
servarr_models::{
|
||||||
|
DiskSpace, HostConfig, Indexer, Language, Log, LogResponse, QualityProfile, QueueEvent,
|
||||||
|
RootFolder, SecurityConfig, Tag, Update,
|
||||||
|
},
|
||||||
|
sonarr_models::{
|
||||||
|
AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse,
|
||||||
|
Episode, IndexerSettings, Series, SeriesMonitor, SeriesStatus, SeriesType,
|
||||||
|
SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask,
|
||||||
|
SonarrTaskName, SystemStatus,
|
||||||
|
},
|
||||||
|
EnumDisplayStyle, Serdeable,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_episode_display() {
|
||||||
|
let episode = Episode {
|
||||||
|
title: Some("Test Title".to_owned()),
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_str_eq!(Episode::default().to_string(), "");
|
||||||
|
assert_str_eq!(episode.to_string(), "Test Title");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_monitor_display() {
|
||||||
|
assert_str_eq!(SeriesMonitor::Unknown.to_string(), "unknown");
|
||||||
|
assert_str_eq!(SeriesMonitor::All.to_string(), "all");
|
||||||
|
assert_str_eq!(SeriesMonitor::Future.to_string(), "future");
|
||||||
|
assert_str_eq!(SeriesMonitor::Missing.to_string(), "missing");
|
||||||
|
assert_str_eq!(SeriesMonitor::Existing.to_string(), "existing");
|
||||||
|
assert_str_eq!(SeriesMonitor::FirstSeason.to_string(), "firstSeason");
|
||||||
|
assert_str_eq!(SeriesMonitor::LastSeason.to_string(), "lastSeason");
|
||||||
|
assert_str_eq!(SeriesMonitor::LatestSeason.to_string(), "latestSeason");
|
||||||
|
assert_str_eq!(SeriesMonitor::Pilot.to_string(), "pilot");
|
||||||
|
assert_str_eq!(SeriesMonitor::Recent.to_string(), "recent");
|
||||||
|
assert_str_eq!(
|
||||||
|
SeriesMonitor::MonitorSpecials.to_string(),
|
||||||
|
"monitorSpecials"
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SeriesMonitor::UnmonitorSpecials.to_string(),
|
||||||
|
"unmonitorSpecials"
|
||||||
|
);
|
||||||
|
assert_str_eq!(SeriesMonitor::None.to_string(), "none");
|
||||||
|
assert_str_eq!(SeriesMonitor::Skip.to_string(), "skip");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_monitor_to_display_str() {
|
||||||
|
assert_str_eq!(SeriesMonitor::Unknown.to_display_str(), "Unknown");
|
||||||
|
assert_str_eq!(SeriesMonitor::All.to_display_str(), "All Episodes");
|
||||||
|
assert_str_eq!(SeriesMonitor::Future.to_display_str(), "Future Episodes");
|
||||||
|
assert_str_eq!(SeriesMonitor::Missing.to_display_str(), "Missing Episodes");
|
||||||
|
assert_str_eq!(
|
||||||
|
SeriesMonitor::Existing.to_display_str(),
|
||||||
|
"Existing Episodes"
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SeriesMonitor::FirstSeason.to_display_str(),
|
||||||
|
"Only First Season"
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SeriesMonitor::LastSeason.to_display_str(),
|
||||||
|
"Only Last Season"
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SeriesMonitor::LatestSeason.to_display_str(),
|
||||||
|
"Only Latest Season"
|
||||||
|
);
|
||||||
|
assert_str_eq!(SeriesMonitor::Pilot.to_display_str(), "Pilot Episode");
|
||||||
|
assert_str_eq!(SeriesMonitor::Recent.to_display_str(), "Recent Episodes");
|
||||||
|
assert_str_eq!(
|
||||||
|
SeriesMonitor::MonitorSpecials.to_display_str(),
|
||||||
|
"Only Specials"
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SeriesMonitor::UnmonitorSpecials.to_display_str(),
|
||||||
|
"Not Specials"
|
||||||
|
);
|
||||||
|
assert_str_eq!(SeriesMonitor::None.to_display_str(), "None");
|
||||||
|
assert_str_eq!(SeriesMonitor::Skip.to_display_str(), "Skip");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_status_display() {
|
||||||
|
assert_str_eq!(SeriesStatus::Continuing.to_string(), "continuing");
|
||||||
|
assert_str_eq!(SeriesStatus::Ended.to_string(), "ended");
|
||||||
|
assert_str_eq!(SeriesStatus::Upcoming.to_string(), "upcoming");
|
||||||
|
assert_str_eq!(SeriesStatus::Deleted.to_string(), "deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_status_to_display_str() {
|
||||||
|
assert_str_eq!(SeriesStatus::Continuing.to_display_str(), "Continuing");
|
||||||
|
assert_str_eq!(SeriesStatus::Ended.to_display_str(), "Ended");
|
||||||
|
assert_str_eq!(SeriesStatus::Upcoming.to_display_str(), "Upcoming");
|
||||||
|
assert_str_eq!(SeriesStatus::Deleted.to_display_str(), "Deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_type_display() {
|
||||||
|
assert_str_eq!(SeriesType::Standard.to_string(), "standard");
|
||||||
|
assert_str_eq!(SeriesType::Daily.to_string(), "daily");
|
||||||
|
assert_str_eq!(SeriesType::Anime.to_string(), "anime");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_type_to_display_str() {
|
||||||
|
assert_str_eq!(SeriesType::Standard.to_display_str(), "Standard");
|
||||||
|
assert_str_eq!(SeriesType::Daily.to_display_str(), "Daily");
|
||||||
|
assert_str_eq!(SeriesType::Anime.to_display_str(), "Anime");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_history_event_type_display() {
|
||||||
|
assert_str_eq!(SonarrHistoryEventType::Unknown.to_string(), "unknown",);
|
||||||
|
assert_str_eq!(SonarrHistoryEventType::Grabbed.to_string(), "grabbed",);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::SeriesFolderImported.to_string(),
|
||||||
|
"seriesFolderImported",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::DownloadFolderImported.to_string(),
|
||||||
|
"downloadFolderImported",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::DownloadFailed.to_string(),
|
||||||
|
"downloadFailed",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::EpisodeFileDeleted.to_string(),
|
||||||
|
"episodeFileDeleted",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::EpisodeFileRenamed.to_string(),
|
||||||
|
"episodeFileRenamed",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::DownloadIgnored.to_string(),
|
||||||
|
"downloadIgnored",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_history_event_type_to_display_str() {
|
||||||
|
assert_str_eq!(SonarrHistoryEventType::Unknown.to_display_str(), "Unknown",);
|
||||||
|
assert_str_eq!(SonarrHistoryEventType::Grabbed.to_display_str(), "Grabbed",);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::SeriesFolderImported.to_display_str(),
|
||||||
|
"Series Folder Imported",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::DownloadFolderImported.to_display_str(),
|
||||||
|
"Download Folder Imported",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::DownloadFailed.to_display_str(),
|
||||||
|
"Download Failed",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::EpisodeFileDeleted.to_display_str(),
|
||||||
|
"Episode File Deleted",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::EpisodeFileRenamed.to_display_str(),
|
||||||
|
"Episode File Renamed",
|
||||||
|
);
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrHistoryEventType::DownloadIgnored.to_display_str(),
|
||||||
|
"Download Ignored",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_task_name_display() {
|
||||||
|
assert_str_eq!(
|
||||||
|
SonarrTaskName::ApplicationUpdateCheck.to_string(),
|
||||||
|
"ApplicationUpdateCheck"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from() {
|
||||||
|
let sonarr_serdeable = SonarrSerdeable::Value(json!({}));
|
||||||
|
|
||||||
|
let serdeable: Serdeable = Serdeable::from(sonarr_serdeable.clone());
|
||||||
|
|
||||||
|
assert_eq!(serdeable, Serdeable::Sonarr(sonarr_serdeable));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_unit() {
|
||||||
|
let sonarr_serdeable = SonarrSerdeable::from(());
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Value(json!({})));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_value() {
|
||||||
|
let value = json!({"test": "test"});
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = value.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Value(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_episode() {
|
||||||
|
let episode = Episode {
|
||||||
|
id: 1,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = episode.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Episode(episode));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_episodes() {
|
||||||
|
let episodes = vec![Episode {
|
||||||
|
id: 1,
|
||||||
|
..Episode::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = episodes.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Episodes(episodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_host_config() {
|
||||||
|
let host_config = HostConfig {
|
||||||
|
port: 1234,
|
||||||
|
..HostConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = host_config.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::HostConfig(host_config));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_indexers() {
|
||||||
|
let indexers = vec![Indexer {
|
||||||
|
id: 1,
|
||||||
|
..Indexer::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = indexers.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Indexers(indexers));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_indexer_settings() {
|
||||||
|
let indexer_settings = IndexerSettings {
|
||||||
|
id: 1,
|
||||||
|
..IndexerSettings::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = indexer_settings.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::IndexerSettings(indexer_settings)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_series_vec() {
|
||||||
|
let series_vec = vec![Series {
|
||||||
|
id: 1,
|
||||||
|
..Series::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = series_vec.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::SeriesVec(series_vec));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_series() {
|
||||||
|
let series = Series {
|
||||||
|
id: 1,
|
||||||
|
..Series::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = series.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Series(series));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_sonarr_history_items() {
|
||||||
|
let history_items = vec![SonarrHistoryItem {
|
||||||
|
id: 1,
|
||||||
|
..SonarrHistoryItem::default()
|
||||||
|
}];
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = history_items.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::SonarrHistoryItems(history_items)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_system_status() {
|
||||||
|
let system_status = SystemStatus {
|
||||||
|
version: "1".to_owned(),
|
||||||
|
..SystemStatus::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = system_status.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::SystemStatus(system_status)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_add_series_search_results() {
|
||||||
|
let add_series_search_results = vec![AddSeriesSearchResult {
|
||||||
|
tvdb_id: 1,
|
||||||
|
..AddSeriesSearchResult::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = add_series_search_results.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::AddSeriesSearchResults(add_series_search_results)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_blocklist_response() {
|
||||||
|
let blocklist_response = BlocklistResponse {
|
||||||
|
records: vec![BlocklistItem {
|
||||||
|
id: 1,
|
||||||
|
..BlocklistItem::default()
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = blocklist_response.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::BlocklistResponse(blocklist_response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_downloads_response() {
|
||||||
|
let downloads_response = DownloadsResponse {
|
||||||
|
records: vec![DownloadRecord {
|
||||||
|
id: 1,
|
||||||
|
..DownloadRecord::default()
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = downloads_response.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::DownloadsResponse(downloads_response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_disk_spaces() {
|
||||||
|
let disk_spaces = vec![DiskSpace {
|
||||||
|
free_space: 1,
|
||||||
|
total_space: 1,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = disk_spaces.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::DiskSpaces(disk_spaces));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_language_profiles() {
|
||||||
|
let language_profiles = vec![
|
||||||
|
Language {
|
||||||
|
id: 1,
|
||||||
|
name: "English".to_owned(),
|
||||||
|
},
|
||||||
|
Language {
|
||||||
|
id: 2,
|
||||||
|
name: "Japanese".to_owned(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = language_profiles.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::LanguageProfiles(language_profiles)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_log_response() {
|
||||||
|
let log_response = LogResponse {
|
||||||
|
records: vec![Log {
|
||||||
|
level: "info".to_owned(),
|
||||||
|
..Log::default()
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = log_response.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::LogResponse(log_response));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_quality_profiles() {
|
||||||
|
let quality_profiles = vec![QualityProfile {
|
||||||
|
name: "Test Profile".to_owned(),
|
||||||
|
id: 1,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = quality_profiles.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::QualityProfiles(quality_profiles)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_queue_events() {
|
||||||
|
let queue_events = vec![QueueEvent {
|
||||||
|
trigger: "test".to_owned(),
|
||||||
|
..QueueEvent::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = queue_events.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::QueueEvents(queue_events));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_releases() {
|
||||||
|
let releases = vec![SonarrRelease {
|
||||||
|
size: 1,
|
||||||
|
..SonarrRelease::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = releases.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Releases(releases));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_root_folders() {
|
||||||
|
let root_folders = vec![RootFolder {
|
||||||
|
id: 1,
|
||||||
|
..RootFolder::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = root_folders.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::RootFolders(root_folders));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_security_config() {
|
||||||
|
let security_config = SecurityConfig {
|
||||||
|
username: Some("Test".to_owned()),
|
||||||
|
..SecurityConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = security_config.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::SecurityConfig(security_config)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_tag() {
|
||||||
|
let tag = Tag {
|
||||||
|
id: 1,
|
||||||
|
..Tag::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = tag.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Tag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_tags() {
|
||||||
|
let tags = vec![Tag {
|
||||||
|
id: 1,
|
||||||
|
..Tag::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = tags.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Tags(tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_tasks() {
|
||||||
|
let tasks = vec![SonarrTask {
|
||||||
|
name: "test".to_owned(),
|
||||||
|
..SonarrTask::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = tasks.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Tasks(tasks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_updates() {
|
||||||
|
let updates = vec![Update {
|
||||||
|
version: "test".to_owned(),
|
||||||
|
..Update::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = updates.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Updates(updates));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_indexer_test_results() {
|
||||||
|
let indexer_test_results = vec![IndexerTestResult {
|
||||||
|
id: 1,
|
||||||
|
..IndexerTestResult::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = indexer_test_results.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::IndexerTestResults(indexer_test_results)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use managarr_tree_widget::{TreeItem, TreeState};
|
||||||
|
use ratatui::text::ToText;
|
||||||
|
|
||||||
|
use super::Scrollable;
|
||||||
|
use core::hash::Hash;
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "stateful_tree_tests.rs"]
|
||||||
|
mod stateful_tree_tests;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct StatefulTree<T>
|
||||||
|
where
|
||||||
|
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display + PartialEq + Eq,
|
||||||
|
{
|
||||||
|
pub state: TreeState,
|
||||||
|
// Allowing the existence of this struct for now, since it may become useful
|
||||||
|
// for future UI developments with additional Servarrs
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub items: Vec<TreeItem<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowing the existence of this struct for now, since it may become useful
|
||||||
|
// for future UI developments with additional Servarrs
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl<T> StatefulTree<T>
|
||||||
|
where
|
||||||
|
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display + PartialEq + Eq,
|
||||||
|
{
|
||||||
|
pub fn set_items(&mut self, items: Vec<TreeItem<T>>) {
|
||||||
|
self.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_selection(&self) -> Option<&T> {
|
||||||
|
self
|
||||||
|
.state
|
||||||
|
.flatten(&self.items)
|
||||||
|
.into_iter()
|
||||||
|
.find(|i| self.state.selected() == i.identifier)
|
||||||
|
.map(|item| item.item.content())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.items.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Scrollable for StatefulTree<T>
|
||||||
|
where
|
||||||
|
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display,
|
||||||
|
{
|
||||||
|
fn scroll_down(&mut self) {
|
||||||
|
self.state.key_down();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_up(&mut self) {
|
||||||
|
self.state.key_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_to_top(&mut self) {
|
||||||
|
self.state.select_first();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_to_bottom(&mut self) {
|
||||||
|
self.state.select_last();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
|
||||||
|
use crate::models::stateful_tree::StatefulTree;
|
||||||
|
use crate::models::Scrollable;
|
||||||
|
use managarr_tree_widget::{Tree, TreeItem, TreeState};
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::widgets::StatefulWidget;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_scrolling_on_empty_tree_performs_no_op() {
|
||||||
|
let mut stateful_tree: StatefulTree<&str> = StatefulTree::default();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), Vec::<u64>::new());
|
||||||
|
|
||||||
|
stateful_tree.scroll_up();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), Vec::<u64>::new());
|
||||||
|
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), Vec::<u64>::new());
|
||||||
|
|
||||||
|
stateful_tree.scroll_to_top();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), Vec::<u64>::new());
|
||||||
|
|
||||||
|
stateful_tree.scroll_to_bottom();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_scroll() {
|
||||||
|
let mut stateful_tree = create_test_stateful_tree();
|
||||||
|
let hash = |s: &str| {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
s.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
};
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 1")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 2")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 3")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 3")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_up();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 2")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_up();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 1")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_to_bottom();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 3")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_to_top();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 1")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_set_items() {
|
||||||
|
let items_vec = vec![
|
||||||
|
TreeItem::new_leaf("Test 1"),
|
||||||
|
TreeItem::new_leaf("Test 2"),
|
||||||
|
TreeItem::new_leaf("Test 3"),
|
||||||
|
];
|
||||||
|
let hash = |s: &str| {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
s.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
};
|
||||||
|
let mut stateful_tree: StatefulTree<&str> = StatefulTree::default();
|
||||||
|
|
||||||
|
stateful_tree.set_items(items_vec.clone());
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 1")]);
|
||||||
|
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.set_items(items_vec.clone());
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 2")]);
|
||||||
|
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.set_items(items_vec);
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 3")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_current_selection() {
|
||||||
|
let mut stateful_tree = create_test_stateful_tree();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
let current_selection = stateful_tree.current_selection();
|
||||||
|
|
||||||
|
assert!(current_selection.is_some());
|
||||||
|
assert_str_eq!(current_selection.unwrap(), stateful_tree.items[0].content());
|
||||||
|
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
let current_selection = stateful_tree.current_selection();
|
||||||
|
|
||||||
|
assert!(current_selection.is_some());
|
||||||
|
assert_str_eq!(current_selection.unwrap(), stateful_tree.items[1].content());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_is_empty() {
|
||||||
|
let mut stateful_tree = create_test_stateful_tree();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert!(!stateful_tree.is_empty());
|
||||||
|
|
||||||
|
stateful_tree = StatefulTree::default();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert!(stateful_tree.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(state: &mut TreeState, items: &[TreeItem<&str>]) {
|
||||||
|
let tree = Tree::new(items).unwrap();
|
||||||
|
let area = Rect::new(0, 0, 10, 4);
|
||||||
|
let mut buffer = Buffer::empty(area);
|
||||||
|
StatefulWidget::render(tree, area, &mut buffer, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_stateful_tree() -> StatefulTree<&'static str> {
|
||||||
|
let mut stateful_tree = StatefulTree::default();
|
||||||
|
stateful_tree.set_items(vec![
|
||||||
|
TreeItem::new_leaf("Test 1"),
|
||||||
|
TreeItem::new_leaf("Test 2"),
|
||||||
|
TreeItem::new_leaf("Test 3"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
stateful_tree
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
-10
@@ -8,39 +8,46 @@ use regex::Regex;
|
|||||||
use reqwest::{Client, RequestBuilder};
|
use reqwest::{Client, RequestBuilder};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use sonarr_network::SonarrEvent;
|
||||||
use strum_macros::Display;
|
use strum_macros::Display;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio::sync::{Mutex, MutexGuard};
|
use tokio::sync::{Mutex, MutexGuard};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::{App, ServarrConfig};
|
||||||
use crate::models::Serdeable;
|
use crate::models::Serdeable;
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
pub mod radarr_network;
|
pub mod radarr_network;
|
||||||
|
pub mod sonarr_network;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "network_tests.rs"]
|
#[path = "network_tests.rs"]
|
||||||
mod network_tests;
|
mod network_tests;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
||||||
pub enum NetworkEvent {
|
|
||||||
Radarr(RadarrEvent),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait NetworkTrait {
|
pub trait NetworkTrait {
|
||||||
async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result<Serdeable>;
|
async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result<Serdeable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait NetworkResource {
|
||||||
|
fn resource(&self) -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
|
pub enum NetworkEvent {
|
||||||
|
Radarr(RadarrEvent),
|
||||||
|
Sonarr(SonarrEvent),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Network<'a, 'b> {
|
pub struct Network<'a, 'b> {
|
||||||
client: Client,
|
client: Client,
|
||||||
cancellation_token: CancellationToken,
|
pub cancellation_token: CancellationToken,
|
||||||
pub app: &'a Arc<Mutex<App<'b>>>,
|
pub app: &'a Arc<Mutex<App<'b>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +59,10 @@ impl<'a, 'b> NetworkTrait for Network<'a, 'b> {
|
|||||||
.handle_radarr_event(radarr_event)
|
.handle_radarr_event(radarr_event)
|
||||||
.await
|
.await
|
||||||
.map(Serdeable::from),
|
.map(Serdeable::from),
|
||||||
|
NetworkEvent::Sonarr(sonarr_event) => self
|
||||||
|
.handle_sonarr_event(sonarr_event)
|
||||||
|
.await
|
||||||
|
.map(Serdeable::from),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut app = self.app.lock().await;
|
let mut app = self.app.lock().await;
|
||||||
@@ -74,6 +85,10 @@ impl<'a, 'b> Network<'a, 'b> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn reset_cancellation_token(&mut self) {
|
||||||
|
self.cancellation_token = self.app.lock().await.reset_cancellation_token();
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_request<B, R>(
|
async fn handle_request<B, R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
request_props: RequestProps<B>,
|
request_props: RequestProps<B>,
|
||||||
@@ -89,9 +104,6 @@ impl<'a, 'b> Network<'a, 'b> {
|
|||||||
select! {
|
select! {
|
||||||
_ = self.cancellation_token.cancelled() => {
|
_ = self.cancellation_token.cancelled() => {
|
||||||
warn!("Received Cancel request. Cancelling request to: {request_uri}");
|
warn!("Received Cancel request. Cancelling request to: {request_uri}");
|
||||||
let mut app = self.app.lock().await;
|
|
||||||
self.cancellation_token = app.reset_cancellation_token();
|
|
||||||
app.is_loading = false;
|
|
||||||
Ok(R::default())
|
Ok(R::default())
|
||||||
}
|
}
|
||||||
resp = self.call_api(request_props).await.send() => {
|
resp = self.call_api(request_props).await.send() => {
|
||||||
@@ -179,6 +191,71 @@ impl<'a, 'b> Network<'a, 'b> {
|
|||||||
.header("X-Api-Key", api_token),
|
.header("X-Api-Key", api_token),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn request_props_from<T, N>(
|
||||||
|
&self,
|
||||||
|
network_event: N,
|
||||||
|
method: RequestMethod,
|
||||||
|
body: Option<T>,
|
||||||
|
path: Option<String>,
|
||||||
|
query_params: Option<String>,
|
||||||
|
) -> RequestProps<T>
|
||||||
|
where
|
||||||
|
T: Serialize + Debug,
|
||||||
|
N: Into<NetworkEvent> + NetworkResource,
|
||||||
|
{
|
||||||
|
let app = self.app.lock().await;
|
||||||
|
let resource = network_event.resource();
|
||||||
|
let (
|
||||||
|
ServarrConfig {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
uri,
|
||||||
|
api_token,
|
||||||
|
ssl_cert_path,
|
||||||
|
},
|
||||||
|
default_port,
|
||||||
|
) = match network_event.into() {
|
||||||
|
NetworkEvent::Radarr(_) => (
|
||||||
|
&app.config.radarr.as_ref().expect("Radarr config undefined"),
|
||||||
|
7878,
|
||||||
|
),
|
||||||
|
NetworkEvent::Sonarr(_) => (
|
||||||
|
&app.config.sonarr.as_ref().expect("Sonarr config undefined"),
|
||||||
|
8989,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let mut uri = if let Some(servarr_uri) = uri {
|
||||||
|
format!("{servarr_uri}/api/v3{resource}")
|
||||||
|
} else {
|
||||||
|
let protocol = if ssl_cert_path.is_some() {
|
||||||
|
"https"
|
||||||
|
} else {
|
||||||
|
"http"
|
||||||
|
};
|
||||||
|
let host = host.as_ref().unwrap();
|
||||||
|
format!(
|
||||||
|
"{protocol}://{host}:{}/api/v3{resource}",
|
||||||
|
port.unwrap_or(default_port)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(path) = path {
|
||||||
|
uri = format!("{uri}{path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(params) = query_params {
|
||||||
|
uri = format!("{uri}?{params}");
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestProps {
|
||||||
|
uri,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
api_token: api_token.to_owned(),
|
||||||
|
ignore_status_code: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ mod tests {
|
|||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::app::{App, AppConfig, RadarrConfig};
|
use crate::app::{App, AppConfig, ServarrConfig};
|
||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
|
use crate::network::NetworkResource;
|
||||||
use crate::network::{Network, NetworkEvent, NetworkTrait, RequestMethod, RequestProps};
|
use crate::network::{Network, NetworkEvent, NetworkTrait, RequestMethod, RequestProps};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -26,7 +28,7 @@ mod tests {
|
|||||||
.with_body("{}")
|
.with_body("{}")
|
||||||
.create_async()
|
.create_async()
|
||||||
.await;
|
.await;
|
||||||
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned();
|
let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
|
||||||
let port = Some(
|
let port = Some(
|
||||||
server.host_with_port().split(':').collect::<Vec<&str>>()[1]
|
server.host_with_port().split(':').collect::<Vec<&str>>()[1]
|
||||||
.parse()
|
.parse()
|
||||||
@@ -34,14 +36,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let mut app = App::default();
|
let mut app = App::default();
|
||||||
app.is_loading = true;
|
app.is_loading = true;
|
||||||
let radarr_config = RadarrConfig {
|
let radarr_config = ServarrConfig {
|
||||||
host,
|
host,
|
||||||
api_token: String::new(),
|
api_token: String::new(),
|
||||||
port,
|
port,
|
||||||
use_ssl: false,
|
|
||||||
ssl_cert_path: None,
|
ssl_cert_path: None,
|
||||||
|
..ServarrConfig::default()
|
||||||
};
|
};
|
||||||
app.config.radarr = radarr_config;
|
app.config.radarr = Some(radarr_config);
|
||||||
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(), Client::new());
|
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
|
||||||
@@ -181,11 +183,31 @@ 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!(resp.is_ok());
|
assert!(resp.is_ok());
|
||||||
assert_eq!(resp.unwrap(), Test::default());
|
assert_eq!(resp.unwrap(), Test::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reset_cancellation_token() {
|
||||||
|
let cancellation_token = CancellationToken::new();
|
||||||
|
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::new(
|
||||||
|
tx,
|
||||||
|
AppConfig::default(),
|
||||||
|
cancellation_token.clone(),
|
||||||
|
)));
|
||||||
|
app_arc.lock().await.should_refresh = false;
|
||||||
|
app_arc.lock().await.is_loading = true;
|
||||||
|
let mut network = Network::new(&app_arc, cancellation_token, Client::new());
|
||||||
|
network.cancellation_token.cancel();
|
||||||
|
|
||||||
|
network.reset_cancellation_token().await;
|
||||||
|
|
||||||
|
assert!(!network.cancellation_token.is_cancelled());
|
||||||
|
assert!(app_arc.lock().await.should_refresh);
|
||||||
|
assert!(!app_arc.lock().await.is_loading);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_handle_request_get_invalid_body() {
|
async fn test_handle_request_get_invalid_body() {
|
||||||
let mut server = Server::new_async().await;
|
let mut server = Server::new_async().await;
|
||||||
@@ -375,6 +397,256 @@ mod tests {
|
|||||||
async_server.assert_async().await;
|
async_server.assert_async().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[should_panic(expected = "Radarr config undefined")]
|
||||||
|
async fn test_request_props_from_requires_radarr_config_to_be_present_for_radarr_events() {
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
|
||||||
|
network
|
||||||
|
.request_props_from(
|
||||||
|
RadarrEvent::GetMovies,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[should_panic(expected = "Sonarr config undefined")]
|
||||||
|
async fn test_request_props_from_requires_sonarr_config_to_be_present_for_sonarr_events() {
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
|
||||||
|
network
|
||||||
|
.request_props_from(
|
||||||
|
SonarrEvent::ListSeries,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(RadarrEvent::GetMovies, 7878)]
|
||||||
|
#[case(SonarrEvent::ListSeries, 8989)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_props_from_default_config(
|
||||||
|
#[case] network_event: impl Into<NetworkEvent> + NetworkResource,
|
||||||
|
#[case] default_port: u16,
|
||||||
|
) {
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
let resource = network_event.resource();
|
||||||
|
app_arc.lock().await.config = AppConfig {
|
||||||
|
radarr: Some(ServarrConfig::default()),
|
||||||
|
sonarr: Some(ServarrConfig::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_props = network
|
||||||
|
.request_props_from(network_event, RequestMethod::Get, None::<()>, None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
request_props.uri,
|
||||||
|
format!("http://localhost:{default_port}/api/v3{resource}")
|
||||||
|
);
|
||||||
|
assert_eq!(request_props.method, RequestMethod::Get);
|
||||||
|
assert_eq!(request_props.body, None);
|
||||||
|
assert!(request_props.api_token.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_props_from_custom_config(
|
||||||
|
#[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into<NetworkEvent>
|
||||||
|
+ NetworkResource,
|
||||||
|
) {
|
||||||
|
let api_token = "testToken1234".to_owned();
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let resource = network_event.resource();
|
||||||
|
let servarr_config = ServarrConfig {
|
||||||
|
host: Some("192.168.0.123".to_owned()),
|
||||||
|
port: Some(8080),
|
||||||
|
api_token: api_token.clone(),
|
||||||
|
ssl_cert_path: Some("/test/cert.crt".to_owned()),
|
||||||
|
..ServarrConfig::default()
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut app = app_arc.lock().await;
|
||||||
|
app.config.radarr = Some(servarr_config.clone());
|
||||||
|
app.config.sonarr = Some(servarr_config);
|
||||||
|
}
|
||||||
|
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
|
||||||
|
let request_props = network
|
||||||
|
.request_props_from(network_event, RequestMethod::Get, None::<()>, None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
request_props.uri,
|
||||||
|
format!("https://192.168.0.123:8080/api/v3{resource}")
|
||||||
|
);
|
||||||
|
assert_eq!(request_props.method, RequestMethod::Get);
|
||||||
|
assert_eq!(request_props.body, None);
|
||||||
|
assert_str_eq!(request_props.api_token, api_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_props_from_custom_config_using_uri_instead_of_host_and_port(
|
||||||
|
#[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into<NetworkEvent>
|
||||||
|
+ NetworkResource,
|
||||||
|
) {
|
||||||
|
let api_token = "testToken1234".to_owned();
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let resource = network_event.resource();
|
||||||
|
let servarr_config = ServarrConfig {
|
||||||
|
uri: Some("https://192.168.0.123:8080".to_owned()),
|
||||||
|
api_token: api_token.clone(),
|
||||||
|
..ServarrConfig::default()
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut app = app_arc.lock().await;
|
||||||
|
app.config.radarr = Some(servarr_config.clone());
|
||||||
|
app.config.sonarr = Some(servarr_config);
|
||||||
|
}
|
||||||
|
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
|
||||||
|
let request_props = network
|
||||||
|
.request_props_from(network_event, RequestMethod::Get, None::<()>, None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
request_props.uri,
|
||||||
|
format!("https://192.168.0.123:8080/api/v3{resource}")
|
||||||
|
);
|
||||||
|
assert_eq!(request_props.method, RequestMethod::Get);
|
||||||
|
assert_eq!(request_props.body, None);
|
||||||
|
assert_str_eq!(request_props.api_token, api_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(RadarrEvent::GetMovies, 7878)]
|
||||||
|
#[case(SonarrEvent::ListSeries, 8989)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_props_from_default_config_with_path_and_query_params(
|
||||||
|
#[case] network_event: impl Into<NetworkEvent> + NetworkResource,
|
||||||
|
#[case] default_port: u16,
|
||||||
|
) {
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
let resource = network_event.resource();
|
||||||
|
app_arc.lock().await.config = AppConfig {
|
||||||
|
radarr: Some(ServarrConfig::default()),
|
||||||
|
sonarr: Some(ServarrConfig::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_props = network
|
||||||
|
.request_props_from(
|
||||||
|
network_event,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
Some("/test".to_owned()),
|
||||||
|
Some("id=1".to_owned()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
request_props.uri,
|
||||||
|
format!("http://localhost:{default_port}/api/v3{resource}/test?id=1")
|
||||||
|
);
|
||||||
|
assert_eq!(request_props.method, RequestMethod::Get);
|
||||||
|
assert_eq!(request_props.body, None);
|
||||||
|
assert!(request_props.api_token.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_props_from_custom_config_with_path_and_query_params(
|
||||||
|
#[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into<NetworkEvent>
|
||||||
|
+ NetworkResource,
|
||||||
|
) {
|
||||||
|
let api_token = "testToken1234".to_owned();
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let resource = network_event.resource();
|
||||||
|
let servarr_config = ServarrConfig {
|
||||||
|
host: Some("192.168.0.123".to_owned()),
|
||||||
|
port: Some(8080),
|
||||||
|
api_token: api_token.clone(),
|
||||||
|
ssl_cert_path: Some("/test/cert.crt".to_owned()),
|
||||||
|
..ServarrConfig::default()
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut app = app_arc.lock().await;
|
||||||
|
app.config.radarr = Some(servarr_config.clone());
|
||||||
|
app.config.sonarr = Some(servarr_config);
|
||||||
|
}
|
||||||
|
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
|
||||||
|
let request_props = network
|
||||||
|
.request_props_from(
|
||||||
|
network_event,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
Some("/test".to_owned()),
|
||||||
|
Some("id=1".to_owned()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
request_props.uri,
|
||||||
|
format!("https://192.168.0.123:8080/api/v3{resource}/test?id=1")
|
||||||
|
);
|
||||||
|
assert_eq!(request_props.method, RequestMethod::Get);
|
||||||
|
assert_eq!(request_props.body, None);
|
||||||
|
assert_str_eq!(request_props.api_token, api_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_props_from_custom_config_using_uri_instead_of_host_and_port_with_path_and_query_params(
|
||||||
|
#[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into<NetworkEvent>
|
||||||
|
+ NetworkResource,
|
||||||
|
) {
|
||||||
|
let api_token = "testToken1234".to_owned();
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||||
|
let resource = network_event.resource();
|
||||||
|
let servarr_config = ServarrConfig {
|
||||||
|
uri: Some("https://192.168.0.123:8080".to_owned()),
|
||||||
|
api_token: api_token.clone(),
|
||||||
|
..ServarrConfig::default()
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut app = app_arc.lock().await;
|
||||||
|
app.config.radarr = Some(servarr_config.clone());
|
||||||
|
app.config.sonarr = Some(servarr_config);
|
||||||
|
}
|
||||||
|
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||||
|
|
||||||
|
let request_props = network
|
||||||
|
.request_props_from(
|
||||||
|
network_event,
|
||||||
|
RequestMethod::Get,
|
||||||
|
None::<()>,
|
||||||
|
Some("/test".to_owned()),
|
||||||
|
Some("id=1".to_owned()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_str_eq!(
|
||||||
|
request_props.uri,
|
||||||
|
format!("https://192.168.0.123:8080/api/v3{resource}/test?id=1")
|
||||||
|
);
|
||||||
|
assert_eq!(request_props.method, RequestMethod::Get);
|
||||||
|
assert_eq!(request_props.body, None);
|
||||||
|
assert_str_eq!(request_props.api_token, api_token);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
|
||||||
struct Test {
|
struct Test {
|
||||||
pub value: String,
|
pub value: String,
|
||||||
@@ -405,3 +677,78 @@ mod tests {
|
|||||||
(async_server, app_arc, server)
|
(async_server, app_arc, server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(in crate::network) mod test_utils {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use mockito::{Matcher, Mock, Server, ServerGuard};
|
||||||
|
use serde_json::Value;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{App, ServarrConfig},
|
||||||
|
network::{NetworkEvent, NetworkResource, RequestMethod},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn mock_servarr_api<'a>(
|
||||||
|
method: RequestMethod,
|
||||||
|
request_body: Option<Value>,
|
||||||
|
response_body: Option<Value>,
|
||||||
|
response_status: Option<usize>,
|
||||||
|
network_event: impl Into<NetworkEvent> + NetworkResource,
|
||||||
|
path: Option<&str>,
|
||||||
|
query_params: Option<&str>,
|
||||||
|
) -> (Mock, Arc<Mutex<App<'a>>>, ServerGuard) {
|
||||||
|
let status = response_status.unwrap_or(200);
|
||||||
|
let resource = network_event.resource();
|
||||||
|
let mut server = Server::new_async().await;
|
||||||
|
let mut uri = format!("/api/v3{resource}");
|
||||||
|
|
||||||
|
if let Some(path) = path {
|
||||||
|
uri = format!("{uri}{path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(params) = query_params {
|
||||||
|
uri = format!("{uri}?{params}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut async_server = server
|
||||||
|
.mock(&method.to_string().to_uppercase(), uri.as_str())
|
||||||
|
.match_header("X-Api-Key", "test1234")
|
||||||
|
.with_status(status);
|
||||||
|
|
||||||
|
if let Some(body) = request_body {
|
||||||
|
async_server = async_server.match_body(Matcher::Json(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(body) = response_body {
|
||||||
|
async_server = async_server.with_body(body.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
async_server = async_server.create_async().await;
|
||||||
|
|
||||||
|
let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
|
||||||
|
let port = Some(
|
||||||
|
server.host_with_port().split(':').collect::<Vec<&str>>()[1]
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let mut app = App::default();
|
||||||
|
let servarr_config = ServarrConfig {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
api_token: "test1234".to_owned(),
|
||||||
|
..ServarrConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match network_event.into() {
|
||||||
|
NetworkEvent::Radarr(_) => app.config.radarr = Some(servarr_config),
|
||||||
|
NetworkEvent::Sonarr(_) => app.config.sonarr = Some(servarr_config),
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_arc = Arc::new(Mutex::new(app));
|
||||||
|
|
||||||
|
(async_server, app_arc, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+357
-397
File diff suppressed because it is too large
Load Diff
+580
-508
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ use crate::models::radarr_models::CollectionMovie;
|
|||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::Route;
|
use crate::models::{EnumDisplayStyle, Route};
|
||||||
use crate::ui::radarr_ui::collections::draw_collections;
|
use crate::ui::radarr_ui::collections::draw_collections;
|
||||||
use crate::ui::styles::ManagarrStyle;
|
use crate::ui::styles::ManagarrStyle;
|
||||||
use crate::ui::utils::{
|
use crate::ui::utils::{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::models::servarr_data::radarr::modals::EditCollectionModal;
|
|||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_BLOCKS,
|
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::Route;
|
use crate::models::{EnumDisplayStyle, Route};
|
||||||
use crate::render_selectable_input_box;
|
use crate::render_selectable_input_box;
|
||||||
use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi;
|
use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi;
|
||||||
use crate::ui::radarr_ui::collections::draw_collections;
|
use crate::ui::radarr_ui::collections::draw_collections;
|
||||||
|
|||||||
@@ -92,7 +92,11 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let percent = 1f64 - (*sizeleft as f64 / *size as f64);
|
let percent = if *size == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
1f64 - (*sizeleft as f64 / *size as f64)
|
||||||
|
};
|
||||||
let file_size: f64 = convert_to_gb(*size);
|
let file_size: f64 = convert_to_gb(*size);
|
||||||
|
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use ratatui::widgets::{Cell, Row};
|
|||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::radarr_models::Indexer;
|
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, INDEXERS_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, INDEXERS_BLOCKS};
|
||||||
|
use crate::models::servarr_models::Indexer;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::ui::radarr_ui::indexers::edit_indexer_ui::EditIndexerUi;
|
use crate::ui::radarr_ui::indexers::edit_indexer_ui::EditIndexerUi;
|
||||||
use crate::ui::radarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi;
|
use crate::ui::radarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES};
|
use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES};
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
|
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::ui::radarr_ui::indexers::draw_indexers;
|
use crate::ui::radarr_ui::indexers::draw_indexers;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::app::radarr::radarr_context_clues::{
|
|||||||
use crate::models::radarr_models::AddMovieSearchResult;
|
use crate::models::radarr_models::AddMovieSearchResult;
|
||||||
use crate::models::servarr_data::radarr::modals::AddMovieModal;
|
use crate::models::servarr_data::radarr::modals::AddMovieModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
|
||||||
use crate::models::Route;
|
use crate::models::{EnumDisplayStyle, Route};
|
||||||
use crate::ui::radarr_ui::collections::{draw_collection_details, draw_collections};
|
use crate::ui::radarr_ui::collections::{draw_collection_details, draw_collections};
|
||||||
use crate::ui::radarr_ui::library::draw_library;
|
use crate::ui::radarr_ui::library::draw_library;
|
||||||
use crate::ui::styles::ManagarrStyle;
|
use crate::ui::styles::ManagarrStyle;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::models::servarr_data::radarr::modals::EditMovieModal;
|
|||||||
use crate::models::servarr_data::radarr::radarr_data::{
|
use crate::models::servarr_data::radarr::radarr_data::{
|
||||||
ActiveRadarrBlock, EDIT_MOVIE_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
ActiveRadarrBlock, EDIT_MOVIE_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::Route;
|
use crate::models::{EnumDisplayStyle, Route};
|
||||||
use crate::render_selectable_input_box;
|
use crate::render_selectable_input_box;
|
||||||
use crate::ui::radarr_ui::library::draw_library;
|
use crate::ui::radarr_ui::library::draw_library;
|
||||||
use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi;
|
use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
|
|||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::radarr_models::{Credit, MovieHistoryItem, Release};
|
use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease};
|
||||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
|
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
@@ -166,6 +166,7 @@ fn draw_file_info(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
|||||||
|
|
||||||
fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
||||||
let block = layout_block_top_border();
|
let block = layout_block_top_border();
|
||||||
|
let unknown_download_status = "Status: Unknown".to_owned();
|
||||||
|
|
||||||
match app.data.radarr_data.movie_details_modal.as_ref() {
|
match app.data.radarr_data.movie_details_modal.as_ref() {
|
||||||
Some(movie_details_modal) if !app.is_loading => {
|
Some(movie_details_modal) if !app.is_loading => {
|
||||||
@@ -182,7 +183,7 @@ fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
|||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|&line| line.starts_with("Status: "))
|
.find(|&line| line.starts_with("Status: "))
|
||||||
.unwrap()
|
.unwrap_or(&unknown_download_status)
|
||||||
.split(": ")
|
.split(": ")
|
||||||
.collect::<Vec<&str>>()[1];
|
.collect::<Vec<&str>>()[1];
|
||||||
let text = Text::from(
|
let text = Text::from(
|
||||||
@@ -285,81 +286,88 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||||
let cast_row_mapping = |cast_member: &Credit| {
|
match app.data.radarr_data.movie_details_modal.as_mut() {
|
||||||
let Credit {
|
Some(movie_details_modal) if !app.is_loading => {
|
||||||
person_name,
|
let cast_row_mapping = |cast_member: &Credit| {
|
||||||
character,
|
let Credit {
|
||||||
..
|
person_name,
|
||||||
} = cast_member;
|
character,
|
||||||
|
..
|
||||||
|
} = cast_member;
|
||||||
|
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(person_name.to_owned()),
|
Cell::from(person_name.to_owned()),
|
||||||
Cell::from(character.clone().unwrap_or_default()),
|
Cell::from(character.clone().unwrap_or_default()),
|
||||||
])
|
])
|
||||||
.success()
|
.success()
|
||||||
};
|
};
|
||||||
let content = Some(
|
let content = Some(&mut movie_details_modal.movie_cast);
|
||||||
&mut app
|
let help_footer = app
|
||||||
.data
|
.data
|
||||||
.radarr_data
|
.radarr_data
|
||||||
.movie_details_modal
|
.movie_info_tabs
|
||||||
.as_mut()
|
.get_active_tab_contextual_help();
|
||||||
.unwrap()
|
let cast_table = ManagarrTable::new(content, cast_row_mapping)
|
||||||
.movie_cast,
|
.block(layout_block_top_border())
|
||||||
);
|
.footer(help_footer)
|
||||||
let help_footer = app
|
.loading(app.is_loading)
|
||||||
.data
|
.headers(["Cast Member", "Character"])
|
||||||
.radarr_data
|
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||||
.movie_info_tabs
|
|
||||||
.get_active_tab_contextual_help();
|
|
||||||
let cast_table = ManagarrTable::new(content, cast_row_mapping)
|
|
||||||
.block(layout_block_top_border())
|
|
||||||
.footer(help_footer)
|
|
||||||
.loading(app.is_loading)
|
|
||||||
.headers(["Cast Member", "Character"])
|
|
||||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
|
||||||
|
|
||||||
f.render_widget(cast_table, area);
|
f.render_widget(cast_table, area);
|
||||||
|
}
|
||||||
|
_ => f.render_widget(
|
||||||
|
LoadingBlock::new(
|
||||||
|
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
|
||||||
|
layout_block_top_border(),
|
||||||
|
),
|
||||||
|
area,
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||||
let crew_row_mapping = |crew_member: &Credit| {
|
match app.data.radarr_data.movie_details_modal.as_mut() {
|
||||||
let Credit {
|
Some(movie_details_modal) if !app.is_loading => {
|
||||||
person_name,
|
let crew_row_mapping = |crew_member: &Credit| {
|
||||||
job,
|
let Credit {
|
||||||
department,
|
person_name,
|
||||||
..
|
job,
|
||||||
} = crew_member;
|
department,
|
||||||
|
..
|
||||||
|
} = crew_member;
|
||||||
|
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(person_name.to_owned()),
|
Cell::from(person_name.to_owned()),
|
||||||
Cell::from(job.clone().unwrap_or_default()),
|
Cell::from(job.clone().unwrap_or_default()),
|
||||||
Cell::from(department.clone().unwrap_or_default()),
|
Cell::from(department.clone().unwrap_or_default()),
|
||||||
])
|
])
|
||||||
.success()
|
.success()
|
||||||
};
|
};
|
||||||
let content = Some(
|
let content = Some(&mut movie_details_modal.movie_crew);
|
||||||
&mut app
|
let help_footer = app
|
||||||
.data
|
.data
|
||||||
.radarr_data
|
.radarr_data
|
||||||
.movie_details_modal
|
.movie_info_tabs
|
||||||
.as_mut()
|
.get_active_tab_contextual_help();
|
||||||
.unwrap()
|
let crew_table = ManagarrTable::new(content, crew_row_mapping)
|
||||||
.movie_crew,
|
.block(layout_block_top_border())
|
||||||
);
|
.loading(app.is_loading)
|
||||||
let help_footer = app
|
.headers(["Crew Member", "Job", "Department"])
|
||||||
.data
|
.constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3))
|
||||||
.radarr_data
|
.footer(help_footer);
|
||||||
.movie_info_tabs
|
|
||||||
.get_active_tab_contextual_help();
|
|
||||||
let crew_table = ManagarrTable::new(content, crew_row_mapping)
|
|
||||||
.block(layout_block_top_border())
|
|
||||||
.loading(app.is_loading)
|
|
||||||
.headers(["Crew Member", "Job", "Department"])
|
|
||||||
.constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3))
|
|
||||||
.footer(help_footer);
|
|
||||||
|
|
||||||
f.render_widget(crew_table, area);
|
f.render_widget(crew_table, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => f.render_widget(
|
||||||
|
LoadingBlock::new(
|
||||||
|
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
|
||||||
|
layout_block_top_border(),
|
||||||
|
),
|
||||||
|
area,
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||||
@@ -372,7 +380,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.clone(),
|
.clone(),
|
||||||
movie_details_modal.movie_releases.items.is_empty(),
|
movie_details_modal.movie_releases.items.is_empty(),
|
||||||
),
|
),
|
||||||
_ => (Release::default(), true),
|
_ => (RadarrRelease::default(), true),
|
||||||
};
|
};
|
||||||
let current_route = *app.get_current_route();
|
let current_route = *app.get_current_route();
|
||||||
let mut default_movie_details_modal = MovieDetailsModal::default();
|
let mut default_movie_details_modal = MovieDetailsModal::default();
|
||||||
@@ -390,8 +398,8 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.unwrap_or(&mut default_movie_details_modal)
|
.unwrap_or(&mut default_movie_details_modal)
|
||||||
.movie_releases,
|
.movie_releases,
|
||||||
);
|
);
|
||||||
let releases_row_mapping = |release: &Release| {
|
let releases_row_mapping = |release: &RadarrRelease| {
|
||||||
let Release {
|
let RadarrRelease {
|
||||||
protocol,
|
protocol,
|
||||||
age,
|
age,
|
||||||
title,
|
title,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user