Compare commits
138 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 |
@@ -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,38 +0,0 @@
|
||||
# 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!
|
||||
|
||||
name: Create minor release
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-plz:
|
||||
# see https://release-plz.ieni.dev/docs/github
|
||||
# for more information
|
||||
name: Release-plz
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Run release-plz
|
||||
uses: MarcoIeni/release-plz-action@v0.5
|
||||
with:
|
||||
bump: minor
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
@@ -1,38 +0,0 @@
|
||||
# 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!
|
||||
|
||||
name: Create patch release
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-plz:
|
||||
# see https://release-plz.ieni.dev/docs/github
|
||||
# for more information
|
||||
name: Release-plz
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Run release-plz
|
||||
uses: MarcoIeni/release-plz-action@v0.5
|
||||
with:
|
||||
bump: patch
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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!
|
||||
|
||||
name: Create major release
|
||||
name: Create release
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -9,8 +9,45 @@ permissions:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump_type:
|
||||
description: "Specify the type of version bump"
|
||||
required: true
|
||||
default: "patch"
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
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:
|
||||
# see https://release-plz.ieni.dev/docs/github
|
||||
# for more information
|
||||
@@ -32,7 +69,7 @@ jobs:
|
||||
- name: Run release-plz
|
||||
uses: MarcoIeni/release-plz-action@v0.5
|
||||
with:
|
||||
bump: major
|
||||
command: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_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
|
||||
+13
-1
@@ -5,7 +5,19 @@ 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/),
|
||||
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
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Contributing
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
## 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
|
||||
|
||||
1. Clone this repo
|
||||
|
||||
Generated
+741
-320
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "managarr"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "A TUI and CLI to manage your Servarrs"
|
||||
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
|
||||
@@ -15,20 +15,20 @@ exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.68"
|
||||
backtrace = "0.3.67"
|
||||
backtrace = "0.3.74"
|
||||
bimap = { version = "0.6.3", features = ["serde"] }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
confy = { version = "0.6.0", default-features = false, features = [
|
||||
"yaml_conf",
|
||||
] }
|
||||
crossterm = "0.27.0"
|
||||
crossterm = "0.28.1"
|
||||
derivative = "2.2.0"
|
||||
human-panic = "1.1.3"
|
||||
human-panic = "2.0.2"
|
||||
indoc = "2.0.0"
|
||||
log = "0.4.17"
|
||||
log4rs = { version = "1.2.0", features = ["file_appender"] }
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.11.14", features = ["json"] }
|
||||
reqwest = { version = "0.12.9", features = ["json"] }
|
||||
serde_yaml = "0.9.16"
|
||||
serde_json = "1.0.91"
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
@@ -36,7 +36,7 @@ strum = { version = "0.26.3", features = ["derive"] }
|
||||
strum_macros = "0.26.4"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
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"
|
||||
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
|
||||
clap_complete = "4.5.33"
|
||||
@@ -45,13 +45,15 @@ ctrlc = "3.4.5"
|
||||
colored = "2.1.0"
|
||||
async-trait = "0.1.83"
|
||||
dirs-next = "2.0.0"
|
||||
managarr-tree-widget = "0.24.0"
|
||||
indicatif = "0.17.9"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.16"
|
||||
mockall = "0.13.0"
|
||||
mockito = "1.0.0"
|
||||
pretty_assertions = "1.3.0"
|
||||
rstest = "0.18.2"
|
||||
rstest = "0.23.0"
|
||||
|
||||
[dev-dependencies.cargo-husky]
|
||||
version = "1"
|
||||
|
||||
@@ -46,34 +46,64 @@ cargo install --locked managarr
|
||||
### Docker
|
||||
Run Managarr as a docker container by mounting your `config.yml` file to `/root/.config/managarr/config.yml`. For example:
|
||||
```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.
|
||||
|
||||
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
|
||||
Key:
|
||||
|
||||
| Symbol | Status |
|
||||
|--------------------|-----------|
|
||||
| :white_check_mark: | Supported |
|
||||
| :x: | Missing |
|
||||
| :clock3: | Planned |
|
||||
| :no_entry_sign: | Won't Add |
|
||||
|
||||
### Radarr
|
||||
|
||||
- [x] View your library, downloads, collections, and blocklist
|
||||
- [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
|
||||
- [x] View your host and security configs from the CLI to programmatically fetch the API token, among other settings
|
||||
- [x] Search your library or collections
|
||||
- [x] Add movies to your library
|
||||
- [x] Delete movies, downloads, and indexers
|
||||
- [x] Trigger automatic searches for movies
|
||||
- [x] Trigger refresh and disk scan for movies, downloads, and collections
|
||||
- [x] Manually search for movies
|
||||
- [x] Edit your movies, collections, and indexers
|
||||
- [x] Manage your tags
|
||||
- [x] Manage your root folders
|
||||
- [x] Manage your blocklist
|
||||
- [x] View and browse logs, tasks, events queues, and updates
|
||||
- [x] Manually trigger scheduled tasks
|
||||
| TUI | CLI | Feature |
|
||||
|--------------------|--------------------|----------------------------------------------------------------------------------------------------------------|
|
||||
| :white_check_mark: | :white_check_mark: | View your library, downloads, collections, and blocklist |
|
||||
| :white_check_mark: | :white_check_mark: | View details of a specific movie including description, history, downloaded file info, or the credits |
|
||||
| :white_check_mark: | :white_check_mark: | View details of any collection and the movies in them |
|
||||
| :no_entry_sign: | :white_check_mark: | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| :white_check_mark: | :white_check_mark: | Search your library or collections |
|
||||
| :white_check_mark: | :white_check_mark: | Add movies to your library |
|
||||
| :white_check_mark: | :white_check_mark: | Delete movies, downloads, and indexers |
|
||||
| :white_check_mark: | :white_check_mark: | Trigger automatic searches for movies |
|
||||
| :white_check_mark: | :white_check_mark: | Trigger refresh and disk scan for movies, downloads, and collections |
|
||||
| :white_check_mark: | :white_check_mark: | Manually search for movies |
|
||||
| :white_check_mark: | :white_check_mark: | Edit your movies, collections, and indexers |
|
||||
| :white_check_mark: | :white_check_mark: | Manage your tags |
|
||||
| :white_check_mark: | :white_check_mark: | Manage your root folders |
|
||||
| :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
|
||||
- [ ] 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
|
||||
|
||||
@@ -105,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
|
||||
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`:
|
||||
|
||||
```shell
|
||||
$ managarr --help
|
||||
managarr 0.2.1
|
||||
managarr 0.3.0
|
||||
Alex Clarke <alex.j.tusa@gmail.com>
|
||||
|
||||
A TUI and CLI to manage your Servarrs
|
||||
@@ -120,43 +150,48 @@ Usage: managarr [OPTIONS] [COMMAND]
|
||||
|
||||
Commands:
|
||||
radarr Commands for manging your Radarr instance
|
||||
sonarr Commands for manging your Sonarr instance
|
||||
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)
|
||||
|
||||
Options:
|
||||
--config <CONFIG> The Managarr configuration file to use
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||
-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
|
||||
$ managarr radarr --help
|
||||
Commands for manging your Radarr instance
|
||||
$ managarr sonarr --help
|
||||
Commands for manging your Sonarr instance
|
||||
|
||||
Usage: managarr radarr [OPTIONS] <COMMAND>
|
||||
Usage: managarr sonarr [OPTIONS] <COMMAND>
|
||||
|
||||
Commands:
|
||||
add Commands to add or create new resources within your Radarr instance
|
||||
delete Commands to delete resources from your Radarr instance
|
||||
edit Commands to edit resources in your Radarr instance
|
||||
get Commands to fetch details of the resources in your Radarr instance
|
||||
list Commands to list attributes from your Radarr instance
|
||||
refresh Commands to refresh the data in your Radarr instance
|
||||
clear-blocklist Clear the blocklist
|
||||
download-release Manually download the given release for the specified movie ID
|
||||
manual-search Trigger a manual search of releases for the movie with the given ID
|
||||
search-new-movie Search for a new film to add to Radarr
|
||||
start-task Start the specified Radarr task
|
||||
test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'
|
||||
test-all-indexers Test all indexers
|
||||
trigger-automatic-search Trigger an automatic search for the movie with the specified ID
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
add Commands to add or create new resources within your Sonarr instance
|
||||
delete Commands to delete resources from your Sonarr instance
|
||||
edit Commands to edit resources in your Sonarr instance
|
||||
get Commands to fetch details of the resources in your Sonarr instance
|
||||
download Commands to download releases in your Sonarr instance
|
||||
list Commands to list attributes from your Sonarr instance
|
||||
refresh Commands to refresh the data in your Sonarr instance
|
||||
manual-search Commands to manually search for releases
|
||||
trigger-automatic-search Commands to trigger automatic searches for releases of different resources in your Sonarr instance
|
||||
clear-blocklist Clear the blocklist
|
||||
mark-history-item-as-failed Mark the Sonarr history item with the given ID as 'failed'
|
||||
search-new-series Search for a new series to add to Sonarr
|
||||
start-task Start the specified Sonarr task
|
||||
test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'
|
||||
test-all-indexers Test all Sonarr indexers
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
--config <CONFIG> The Managarr configuration file to use
|
||||
-h, --help Print help
|
||||
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||
--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:
|
||||
@@ -170,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),
|
||||
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
|
||||
```
|
||||
@@ -236,9 +271,11 @@ tautulli:
|
||||
## Environment Variables
|
||||
Managarr supports using environment variables on startup so you don't have to always specify certain flags:
|
||||
|
||||
| Variable | Description | Equivalent Flag |
|
||||
| --------------------------------------- | -------------------------------- | -------------------------------- |
|
||||
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
|
||||
| Variable | Description | Equivalent Flag |
|
||||
|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
|
||||
| `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!)
|
||||
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 @@
|
||||
{}
|
||||
+61
-16
@@ -5,9 +5,10 @@ mod tests {
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
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::{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::NetworkEvent;
|
||||
|
||||
@@ -34,7 +35,7 @@ mod tests {
|
||||
},
|
||||
TabRoute {
|
||||
title: "Sonarr",
|
||||
route: Route::Sonarr,
|
||||
route: ActiveSonarrBlock::Series.into(),
|
||||
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
|
||||
contextual_help: None,
|
||||
},
|
||||
@@ -47,6 +48,7 @@ mod tests {
|
||||
assert!(!app.is_routing);
|
||||
assert!(!app.should_refresh);
|
||||
assert!(!app.should_ignore_quit_key);
|
||||
assert!(!app.cli_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -87,7 +89,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
|
||||
assert!(app.cancellation_token.is_cancelled());
|
||||
@@ -96,6 +102,8 @@ mod tests {
|
||||
|
||||
assert!(!app.cancellation_token.is_cancelled());
|
||||
assert!(!new_token.is_cancelled());
|
||||
assert!(!app.is_loading);
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -120,6 +128,10 @@ mod tests {
|
||||
version: "test".to_owned(),
|
||||
..RadarrData::default()
|
||||
},
|
||||
sonarr_data: SonarrData {
|
||||
version: "test".to_owned(),
|
||||
..SonarrData::default()
|
||||
},
|
||||
},
|
||||
..App::default()
|
||||
};
|
||||
@@ -129,6 +141,7 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert_eq!(app.error, HorizontallyScrollableText::default());
|
||||
assert!(app.data.radarr_data.version.is_empty());
|
||||
assert!(app.data.sonarr_data.version.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -145,6 +158,29 @@ mod tests {
|
||||
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]
|
||||
async fn test_on_tick_first_render() {
|
||||
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
@@ -158,6 +194,7 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
app.on_tick(true).await;
|
||||
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetQualityProfiles.into()
|
||||
@@ -172,7 +209,11 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
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!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
@@ -182,10 +223,6 @@ mod tests {
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetMovies.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(!app.is_routing);
|
||||
assert!(!app.should_refresh);
|
||||
assert_eq!(app.tick_count, 1);
|
||||
@@ -218,13 +255,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radarr_config_default() {
|
||||
let radarr_config = RadarrConfig::default();
|
||||
fn test_app_config_default() {
|
||||
let app_config = AppConfig::default();
|
||||
|
||||
assert_eq!(radarr_config.host, Some("localhost".to_string()));
|
||||
assert_eq!(radarr_config.port, Some(7878));
|
||||
assert_eq!(radarr_config.uri, None);
|
||||
assert!(radarr_config.api_token.is_empty());
|
||||
assert_eq!(radarr_config.ssl_cert_path, None);
|
||||
assert!(app_config.radarr.is_none());
|
||||
assert!(app_config.sonarr.is_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);
|
||||
}
|
||||
}
|
||||
|
||||
+51
-18
@@ -8,7 +8,9 @@ use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
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::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
|
||||
use crate::network::NetworkEvent;
|
||||
|
||||
@@ -35,6 +37,7 @@ pub struct App<'a> {
|
||||
pub is_loading: bool,
|
||||
pub should_refresh: bool,
|
||||
pub should_ignore_quit_key: bool,
|
||||
pub cli_mode: bool,
|
||||
pub config: AppConfig,
|
||||
pub data: Data<'a>,
|
||||
}
|
||||
@@ -56,7 +59,10 @@ impl<'a> App<'a> {
|
||||
pub async fn dispatch_network_event(&mut self, action: NetworkEvent) {
|
||||
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 Err(e) = network_tx.send(action).await {
|
||||
self.is_loading = false;
|
||||
@@ -113,6 +119,8 @@ impl<'a> App<'a> {
|
||||
|
||||
pub fn reset_cancellation_token(&mut self) -> CancellationToken {
|
||||
self.cancellation_token = CancellationToken::new();
|
||||
self.should_refresh = true;
|
||||
self.is_loading = false;
|
||||
|
||||
self.cancellation_token.clone()
|
||||
}
|
||||
@@ -146,7 +154,7 @@ impl<'a> Default for App<'a> {
|
||||
},
|
||||
TabRoute {
|
||||
title: "Sonarr",
|
||||
route: Route::Sonarr,
|
||||
route: ActiveSonarrBlock::Series.into(),
|
||||
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
|
||||
contextual_help: None,
|
||||
},
|
||||
@@ -158,6 +166,7 @@ impl<'a> Default for App<'a> {
|
||||
is_routing: false,
|
||||
should_refresh: false,
|
||||
should_ignore_quit_key: false,
|
||||
cli_mode: false,
|
||||
config: AppConfig::default(),
|
||||
data: Data::default(),
|
||||
}
|
||||
@@ -167,25 +176,49 @@ impl<'a> Default for App<'a> {
|
||||
#[derive(Default)]
|
||||
pub struct Data<'a> {
|
||||
pub radarr_data: RadarrData<'a>,
|
||||
pub sonarr_data: SonarrData,
|
||||
}
|
||||
|
||||
pub trait ServarrConfig {
|
||||
fn validate(&self);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Default)]
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub radarr: RadarrConfig,
|
||||
pub radarr: Option<ServarrConfig>,
|
||||
pub sonarr: Option<ServarrConfig>,
|
||||
}
|
||||
|
||||
impl ServarrConfig for AppConfig {
|
||||
fn validate(&self) {
|
||||
self.radarr.validate();
|
||||
impl AppConfig {
|
||||
pub fn validate(&self) {
|
||||
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)]
|
||||
pub struct RadarrConfig {
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ServarrConfig {
|
||||
pub host: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub uri: Option<String>,
|
||||
@@ -193,20 +226,20 @@ pub struct RadarrConfig {
|
||||
pub ssl_cert_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ServarrConfig 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 Radarr configuration".to_owned());
|
||||
log_and_print_error("'host' or 'uri' is required for configuration".to_owned());
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RadarrConfig {
|
||||
impl Default for ServarrConfig {
|
||||
fn default() -> Self {
|
||||
RadarrConfig {
|
||||
ServarrConfig {
|
||||
host: Some("localhost".to_string()),
|
||||
port: Some(7878),
|
||||
port: None,
|
||||
uri: None,
|
||||
api_token: "".to_string(),
|
||||
ssl_cert_path: None,
|
||||
|
||||
+12
-19
@@ -142,35 +142,22 @@ impl<'a> App<'a> {
|
||||
is_first_render: bool,
|
||||
) {
|
||||
if is_first_render {
|
||||
self
|
||||
.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.refresh_metadata().await;
|
||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||
}
|
||||
|
||||
if self.should_refresh {
|
||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||
self.refresh_metadata().await;
|
||||
}
|
||||
|
||||
if self.is_routing {
|
||||
if self.is_loading && !self.should_refresh {
|
||||
if !self.should_refresh {
|
||||
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 {
|
||||
@@ -191,6 +178,12 @@ impl<'a> App<'a> {
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetDownloads.into())
|
||||
.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) {
|
||||
|
||||
@@ -6,7 +6,7 @@ mod tests {
|
||||
|
||||
use crate::app::radarr::ActiveRadarrBlock;
|
||||
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::network::radarr_network::RadarrEvent;
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
let mut movie_details_modal = MovieDetailsModal::default();
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(vec![Release::default()]);
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
|
||||
app
|
||||
@@ -508,6 +508,14 @@ mod tests {
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -531,16 +539,16 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
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!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetStatus.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
}
|
||||
@@ -549,6 +557,7 @@ mod tests {
|
||||
async fn test_radarr_on_tick_routing() {
|
||||
let (mut app, mut sync_network_rx) = construct_app_unit();
|
||||
app.is_routing = true;
|
||||
app.should_refresh = true;
|
||||
|
||||
app
|
||||
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
|
||||
@@ -574,43 +583,19 @@ mod tests {
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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_loading = true;
|
||||
app.should_refresh = false;
|
||||
|
||||
app
|
||||
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
|
||||
.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());
|
||||
}
|
||||
|
||||
@@ -627,7 +612,6 @@ mod tests {
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(app.should_refresh);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
+57
-10
@@ -10,19 +10,28 @@ mod tests {
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand},
|
||||
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand},
|
||||
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,
|
||||
},
|
||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
network::{
|
||||
radarr_network::RadarrEvent, sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent,
|
||||
},
|
||||
Cli,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr"]);
|
||||
#[rstest]
|
||||
fn test_servarr_subcommand_requires_subcommand(#[values("radarr", "sonarr")] subcommand: &str) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", subcommand]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
@@ -39,6 +48,13 @@ mod tests {
|
||||
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]
|
||||
fn test_completions_requires_argument() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||
@@ -106,8 +122,8 @@ mod tests {
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse(
|
||||
BlocklistResponse {
|
||||
records: vec![BlocklistItem::default()],
|
||||
RadarrBlocklistResponse {
|
||||
records: vec![RadarrBlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
@@ -121,9 +137,40 @@ mod tests {
|
||||
)))
|
||||
});
|
||||
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());
|
||||
}
|
||||
|
||||
+28
-21
@@ -4,11 +4,13 @@ use anyhow::Result;
|
||||
use clap::{command, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cli_tests.rs"]
|
||||
@@ -19,6 +21,9 @@ pub enum Command {
|
||||
#[command(subcommand, about = "Commands for manging your Radarr instance")]
|
||||
Radarr(RadarrCommand),
|
||||
|
||||
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
|
||||
Sonarr(SonarrCommand),
|
||||
|
||||
#[command(
|
||||
arg_required_else_help = true,
|
||||
about = "Generate shell completions for the Managarr CLI"
|
||||
@@ -27,24 +32,39 @@ pub enum Command {
|
||||
#[arg(value_enum)]
|
||||
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>> {
|
||||
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(
|
||||
app: &Arc<Mutex<App<'_>>>,
|
||||
command: Command,
|
||||
network: &mut dyn NetworkTrait,
|
||||
) -> Result<()> {
|
||||
if let Command::Radarr(radarr_command) = command {
|
||||
RadarrCliHandler::with(app, radarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Ok(())
|
||||
) -> Result<String> {
|
||||
let result = match command {
|
||||
Command::Radarr(radarr_command) => {
|
||||
RadarrCliHandler::with(app, radarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Command::Sonarr(sonarr_command) => {
|
||||
SonarrCliHandler::with(app, sonarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -68,16 +88,3 @@ pub fn mutex_flags_or_default(positive: bool, negative: bool, default_value: boo
|
||||
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::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
models::radarr_models::{AddMovieBody, AddOptions, MinimumAvailability, Monitor},
|
||||
models::radarr_models::{AddMovieBody, AddMovieOptions, MinimumAvailability, MovieMonitor},
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
@@ -47,7 +46,7 @@ pub enum RadarrAddCommand {
|
||||
default_value_t = MinimumAvailability::default()
|
||||
)]
|
||||
minimum_availability: MinimumAvailability,
|
||||
#[arg(long, help = "Should Radarr monitor this film")]
|
||||
#[arg(long, help = "Disable monitoring for this film")]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
@@ -60,9 +59,9 @@ pub enum RadarrAddCommand {
|
||||
long,
|
||||
help = "What Radarr should monitor",
|
||||
value_enum,
|
||||
default_value_t = Monitor::default()
|
||||
default_value_t = MovieMonitor::default()
|
||||
)]
|
||||
monitor: Monitor,
|
||||
monitor: MovieMonitor,
|
||||
#[arg(
|
||||
long,
|
||||
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<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrAddCommand::Movie {
|
||||
tmdb_id,
|
||||
root_folder_path,
|
||||
@@ -126,24 +125,33 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
|
||||
minimum_availability: minimum_availability.to_string(),
|
||||
monitored: !disable_monitoring,
|
||||
tags,
|
||||
add_options: AddOptions {
|
||||
add_options: AddMovieOptions {
|
||||
monitor: monitor.to_string(),
|
||||
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 } => {
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::AddRootFolder(Some(root_folder_path.clone()))
|
||||
);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::AddRootFolder(Some(root_folder_path)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
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},
|
||||
Command,
|
||||
},
|
||||
models::radarr_models::{MinimumAvailability, Monitor},
|
||||
models::radarr_models::{MinimumAvailability, MovieMonitor},
|
||||
Cli,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_add_command_from() {
|
||||
@@ -111,6 +112,8 @@ mod tests {
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--tmdb-id",
|
||||
"1",
|
||||
flag,
|
||||
]);
|
||||
|
||||
@@ -187,7 +190,7 @@ mod tests {
|
||||
minimum_availability: MinimumAvailability::default(),
|
||||
disable_monitoring: false,
|
||||
tag: vec![],
|
||||
monitor: Monitor::default(),
|
||||
monitor: MovieMonitor::default(),
|
||||
no_search_for_movie: false,
|
||||
};
|
||||
|
||||
@@ -219,7 +222,7 @@ mod tests {
|
||||
minimum_availability: MinimumAvailability::default(),
|
||||
disable_monitoring: false,
|
||||
tag: vec![1, 2],
|
||||
monitor: Monitor::default(),
|
||||
monitor: MovieMonitor::default(),
|
||||
no_search_for_movie: false,
|
||||
};
|
||||
|
||||
@@ -255,7 +258,7 @@ mod tests {
|
||||
minimum_availability: MinimumAvailability::Released,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: Monitor::MovieAndCollection,
|
||||
monitor: MovieMonitor::MovieAndCollection,
|
||||
no_search_for_movie: true,
|
||||
};
|
||||
|
||||
@@ -356,7 +359,7 @@ mod tests {
|
||||
app::App,
|
||||
cli::{radarr::add_command_handler::RadarrAddCommandHandler, CliCommandHandler},
|
||||
models::{
|
||||
radarr_models::{AddMovieBody, AddOptions, RadarrSerdeable},
|
||||
radarr_models::{AddMovieBody, AddMovieOptions, RadarrSerdeable},
|
||||
Serdeable,
|
||||
},
|
||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
@@ -378,7 +381,7 @@ mod tests {
|
||||
minimum_availability: "released".to_owned(),
|
||||
monitored: false,
|
||||
tags: vec![1, 2],
|
||||
add_options: AddOptions {
|
||||
add_options: AddMovieOptions {
|
||||
monitor: "movieAndCollection".to_owned(),
|
||||
search_for_movie: false,
|
||||
},
|
||||
@@ -403,7 +406,7 @@ mod tests {
|
||||
minimum_availability: MinimumAvailability::Released,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: Monitor::MovieAndCollection,
|
||||
monitor: MovieMonitor::MovieAndCollection,
|
||||
no_search_for_movie: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
models::radarr_models::DeleteMovieParams,
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
@@ -85,19 +84,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))
|
||||
);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
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 } => {
|
||||
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 {
|
||||
movie_id,
|
||||
@@ -109,16 +117,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
|
||||
delete_movie_files: delete_files_from_disk,
|
||||
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 } => {
|
||||
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 } => {
|
||||
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,
|
||||
};
|
||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_delete_command_from() {
|
||||
|
||||
@@ -7,12 +7,11 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
models::{
|
||||
radarr_models::{
|
||||
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
|
||||
MinimumAvailability, RadarrSerdeable,
|
||||
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability, RadarrSerdeable,
|
||||
},
|
||||
servarr_models::EditIndexerParams,
|
||||
Serdeable,
|
||||
},
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
@@ -339,8 +338,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrEditCommand::AllIndexerSettings {
|
||||
allow_hardcoded_subs,
|
||||
disable_allow_hardcoded_subs,
|
||||
@@ -389,11 +388,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
})
|
||||
.into(),
|
||||
};
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::EditAllIndexerSettings(Some(params)),
|
||||
"All indexer settings updated"
|
||||
);
|
||||
self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::EditAllIndexerSettings(Some(params)).into())
|
||||
.await?;
|
||||
"All indexer settings updated".to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
RadarrEditCommand::Collection {
|
||||
@@ -417,11 +418,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
root_folder_path,
|
||||
search_on_add: search_on_add_value,
|
||||
};
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::EditCollection(Some(edit_collection_params)),
|
||||
"Collection Updated"
|
||||
);
|
||||
self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::EditCollection(Some(edit_collection_params)).into())
|
||||
.await?;
|
||||
"Collection updated".to_owned()
|
||||
}
|
||||
RadarrEditCommand::Indexer {
|
||||
indexer_id,
|
||||
@@ -458,11 +459,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::EditIndexer(Some(edit_indexer_params)),
|
||||
"Indexer updated"
|
||||
);
|
||||
self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)).into())
|
||||
.await?;
|
||||
"Indexer updated".to_owned()
|
||||
}
|
||||
RadarrEditCommand::Movie {
|
||||
movie_id,
|
||||
@@ -485,14 +486,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::EditMovie(Some(edit_movie_params)),
|
||||
"Movie updated"
|
||||
);
|
||||
self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::EditMovie(Some(edit_movie_params)).into())
|
||||
.await?;
|
||||
"Movie Updated".to_owned()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ mod tests {
|
||||
Cli,
|
||||
};
|
||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_edit_command_from() {
|
||||
@@ -809,9 +810,10 @@ mod tests {
|
||||
},
|
||||
models::{
|
||||
radarr_models::{
|
||||
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
|
||||
MinimumAvailability, RadarrSerdeable,
|
||||
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability,
|
||||
RadarrSerdeable,
|
||||
},
|
||||
servarr_models::EditIndexerParams,
|
||||
Serdeable,
|
||||
},
|
||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
|
||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
@@ -72,28 +71,52 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
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 => {
|
||||
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 } => {
|
||||
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 } => {
|
||||
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 => {
|
||||
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 => {
|
||||
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)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use clap::error::ErrorKind;
|
||||
use clap::CommandFactory;
|
||||
|
||||
@@ -7,6 +7,7 @@ mod test {
|
||||
use crate::cli::radarr::RadarrCommand;
|
||||
use crate::cli::Command;
|
||||
use crate::Cli;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_get_command_from() {
|
||||
|
||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
@@ -25,6 +24,8 @@ pub enum RadarrListCommand {
|
||||
Collections,
|
||||
#[command(about = "List all active downloads in Radarr")]
|
||||
Downloads,
|
||||
#[command(about = "List disk space details for all provisioned root folders in Radarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "List all Radarr indexers")]
|
||||
Indexers,
|
||||
#[command(about = "Fetch Radarr logs")]
|
||||
@@ -56,7 +57,7 @@ pub enum RadarrListCommand {
|
||||
RootFolders,
|
||||
#[command(about = "List all Radarr tags")]
|
||||
Tags,
|
||||
#[command(about = "List tasks")]
|
||||
#[command(about = "List all Radarr tasks")]
|
||||
Tasks,
|
||||
#[command(about = "List all Radarr updates")]
|
||||
Updates,
|
||||
@@ -87,19 +88,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
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 => {
|
||||
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 => {
|
||||
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 => {
|
||||
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 {
|
||||
events,
|
||||
@@ -113,39 +137,69 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
|
||||
if output_in_log_format {
|
||||
let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone();
|
||||
|
||||
let json = serde_json::to_string_pretty(&log_lines)?;
|
||||
println!("{}", json);
|
||||
serde_json::to_string_pretty(&log_lines)?
|
||||
} else {
|
||||
let json = serde_json::to_string_pretty(&logs)?;
|
||||
println!("{}", json);
|
||||
serde_json::to_string_pretty(&logs)?
|
||||
}
|
||||
}
|
||||
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 } => {
|
||||
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 => {
|
||||
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 => {
|
||||
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 => {
|
||||
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 => {
|
||||
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 => {
|
||||
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 => {
|
||||
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::Command;
|
||||
use crate::Cli;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_list_command_from() {
|
||||
@@ -29,6 +30,7 @@ mod tests {
|
||||
"blocklist",
|
||||
"collections",
|
||||
"downloads",
|
||||
"disk-space",
|
||||
"indexers",
|
||||
"movies",
|
||||
"quality-profiles",
|
||||
@@ -80,8 +82,8 @@ mod tests {
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command {
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
if let Some(Command::Radarr(RadarrCommand::List(credits_command))) = result.unwrap().command {
|
||||
assert_eq!(credits_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +123,7 @@ mod tests {
|
||||
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
|
||||
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
|
||||
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
|
||||
#[case(RadarrListCommand::DiskSpace, RadarrEvent::GetDiskSpace)]
|
||||
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
|
||||
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
|
||||
#[case(RadarrListCommand::QualityProfiles, RadarrEvent::GetQualityProfiles)]
|
||||
@@ -130,7 +133,7 @@ mod tests {
|
||||
#[case(RadarrListCommand::Tasks, RadarrEvent::GetTasks)]
|
||||
#[case(RadarrListCommand::Updates, RadarrEvent::GetUpdates)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_blocklist_command(
|
||||
async fn test_handle_list_command(
|
||||
#[case] list_command: RadarrListCommand,
|
||||
#[case] expected_radarr_event: RadarrEvent,
|
||||
) {
|
||||
|
||||
+50
-18
@@ -12,8 +12,7 @@ use tokio::sync::Mutex;
|
||||
use crate::app::App;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::execute_network_event;
|
||||
use crate::models::radarr_models::{ReleaseDownloadBody, TaskName};
|
||||
use crate::models::radarr_models::{RadarrReleaseDownloadBody, RadarrTaskName};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::NetworkTrait;
|
||||
use anyhow::Result;
|
||||
@@ -86,7 +85,7 @@ pub enum RadarrCommand {
|
||||
ManualSearch {
|
||||
#[arg(
|
||||
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
|
||||
)]
|
||||
movie_id: i64,
|
||||
@@ -108,7 +107,7 @@ pub enum RadarrCommand {
|
||||
value_enum,
|
||||
required = true
|
||||
)]
|
||||
task_name: TaskName,
|
||||
task_name: RadarrTaskName,
|
||||
},
|
||||
#[command(
|
||||
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)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Test all indexers")]
|
||||
#[command(about = "Test all Radarr indexers")]
|
||||
TestAllIndexers,
|
||||
#[command(about = "Trigger an automatic search for the movie with the specified ID")]
|
||||
TriggerAutomaticSearch {
|
||||
@@ -155,8 +154,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrCommand::Add(add_command) => {
|
||||
RadarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||
.handle()
|
||||
@@ -192,41 +191,74 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetBlocklist.into())
|
||||
.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 {
|
||||
guid,
|
||||
indexer_id,
|
||||
movie_id,
|
||||
} => {
|
||||
let params = ReleaseDownloadBody {
|
||||
let params = RadarrReleaseDownloadBody {
|
||||
guid,
|
||||
indexer_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 } => {
|
||||
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 } => {
|
||||
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 } => {
|
||||
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 } => {
|
||||
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 => {
|
||||
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 } => {
|
||||
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());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_download_release_requires_movie_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
@@ -50,7 +50,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_download_release_requires_guid() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
@@ -69,7 +69,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_download_release_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
@@ -105,7 +105,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_manual_search_requires_movie_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "manual-search"]);
|
||||
|
||||
@@ -129,7 +129,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_search_new_movie_requires_query() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "search-new-movie"]);
|
||||
|
||||
@@ -153,7 +153,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_start_task_requires_task_name() {
|
||||
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() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
@@ -191,7 +191,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_test_indexer_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "test-indexer"]);
|
||||
|
||||
@@ -215,7 +215,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_trigger_automatic_search_requires_movie_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "radarr", "trigger-automatic-search"]);
|
||||
@@ -261,8 +261,8 @@ mod tests {
|
||||
},
|
||||
models::{
|
||||
radarr_models::{
|
||||
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrSerdeable, ReleaseDownloadBody,
|
||||
TaskName,
|
||||
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrReleaseDownloadBody,
|
||||
RadarrSerdeable, RadarrTaskName,
|
||||
},
|
||||
Serdeable,
|
||||
},
|
||||
@@ -304,7 +304,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_release_command() {
|
||||
let expected_release_download_body = ReleaseDownloadBody {
|
||||
let expected_release_download_body = RadarrReleaseDownloadBody {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
movie_id: 1,
|
||||
@@ -389,7 +389,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_start_task_command() {
|
||||
let expected_task_name = TaskName::ApplicationCheckUpdate;
|
||||
let expected_task_name = RadarrTaskName::ApplicationCheckUpdate;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
@@ -404,7 +404,7 @@ mod tests {
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
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)
|
||||
|
||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
@@ -19,7 +18,7 @@ mod refresh_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum RadarrRefreshCommand {
|
||||
#[command(about = "Refresh all movie data for all movies in your library")]
|
||||
#[command(about = "Refresh all movie data for all movies in your Radarr library")]
|
||||
AllMovies,
|
||||
#[command(about = "Refresh movie data and scan disk for the movie with the given ID")]
|
||||
Movie {
|
||||
@@ -63,22 +62,38 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand>
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
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 => {
|
||||
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 => {
|
||||
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 } => {
|
||||
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::Command;
|
||||
use crate::Cli;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_refresh_command_from() {
|
||||
@@ -81,7 +82,7 @@ mod tests {
|
||||
#[case(RadarrRefreshCommand::Collections, RadarrEvent::UpdateCollections)]
|
||||
#[case(RadarrRefreshCommand::Downloads, RadarrEvent::UpdateDownloads)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_blocklist_command(
|
||||
async fn test_handle_refresh_command(
|
||||
#[case] refresh_command: RadarrRefreshCommand,
|
||||
#[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::handlers::radarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler};
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{
|
||||
BlocklistItem, BlocklistItemMovie, Language, Quality, QualityWrapper,
|
||||
};
|
||||
use crate::models::radarr_models::{BlocklistItem, BlocklistItemMovie};
|
||||
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;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
@@ -960,6 +959,7 @@ mod tests {
|
||||
id: 3,
|
||||
source_title: "test 1".to_owned(),
|
||||
languages: vec![Language {
|
||||
id: 1,
|
||||
name: "telgu".to_owned(),
|
||||
}],
|
||||
quality: QualityWrapper {
|
||||
@@ -968,6 +968,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
custom_formats: Some(vec![Language {
|
||||
id: 2,
|
||||
name: "nikki".to_owned(),
|
||||
}]),
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
|
||||
@@ -980,6 +981,7 @@ mod tests {
|
||||
id: 2,
|
||||
source_title: "test 2".to_owned(),
|
||||
languages: vec![Language {
|
||||
id: 3,
|
||||
name: "chinese".to_owned(),
|
||||
}],
|
||||
quality: QualityWrapper {
|
||||
@@ -989,9 +991,11 @@ mod tests {
|
||||
},
|
||||
custom_formats: Some(vec![
|
||||
Language {
|
||||
id: 4,
|
||||
name: "alex".to_owned(),
|
||||
},
|
||||
Language {
|
||||
id: 5,
|
||||
name: "English".to_owned(),
|
||||
},
|
||||
]),
|
||||
@@ -1005,6 +1009,7 @@ mod tests {
|
||||
id: 1,
|
||||
source_title: "test 3".to_owned(),
|
||||
languages: vec![Language {
|
||||
id: 1,
|
||||
name: "english".to_owned(),
|
||||
}],
|
||||
quality: QualityWrapper {
|
||||
@@ -1013,6 +1018,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
custom_formats: Some(vec![Language {
|
||||
id: 2,
|
||||
name: "English".to_owned(),
|
||||
}]),
|
||||
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::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
|
||||
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 strum::IntoEnumIterator;
|
||||
|
||||
@@ -14,7 +14,7 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
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::BlockSelectionState;
|
||||
|
||||
@@ -69,7 +69,7 @@ mod tests {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
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 super::*;
|
||||
@@ -334,7 +334,7 @@ mod tests {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
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_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
};
|
||||
@@ -759,7 +759,7 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
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, BlockSelectionState,
|
||||
};
|
||||
@@ -1224,7 +1224,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::app::App;
|
||||
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 rstest::rstest;
|
||||
|
||||
@@ -1281,7 +1281,7 @@ mod tests {
|
||||
|
||||
mod test_handle_key_char {
|
||||
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::BlockSelectionState;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
@@ -9,16 +9,15 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::indexers::IndexersHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::Indexer;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::Indexer;
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::radarr_models::Indexer;
|
||||
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
|
||||
|
||||
use super::*;
|
||||
@@ -65,7 +64,6 @@ mod tests {
|
||||
}
|
||||
|
||||
mod test_handle_home_end {
|
||||
use crate::models::radarr_models::Indexer;
|
||||
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
|
||||
|
||||
use super::*;
|
||||
@@ -239,11 +237,11 @@ mod tests {
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use crate::models::radarr_models::{Indexer, IndexerField};
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::{Indexer, IndexerField};
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::assert_eq;
|
||||
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,
|
||||
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;
|
||||
|
||||
mod edit_indexer_handler;
|
||||
|
||||
@@ -5,7 +5,7 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
|
||||
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::stateful_table::StatefulTable;
|
||||
use strum::IntoEnumIterator;
|
||||
@@ -14,7 +14,7 @@ mod tests {
|
||||
use pretty_assertions::assert_str_eq;
|
||||
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::simple_stateful_iterable_vec;
|
||||
|
||||
@@ -112,7 +112,7 @@ mod tests {
|
||||
|
||||
mod test_handle_home_end {
|
||||
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 pretty_assertions::assert_str_eq;
|
||||
|
||||
|
||||
@@ -8,10 +8,9 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::library::add_movie_handler::AddMovieHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieSearchResult, MinimumAvailability, Monitor, RootFolder,
|
||||
};
|
||||
use crate::models::radarr_models::{AddMovieSearchResult, MinimumAvailability, MovieMonitor};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
@@ -142,7 +141,7 @@ mod tests {
|
||||
fn test_add_movie_select_monitor_scroll(
|
||||
#[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();
|
||||
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
||||
app
|
||||
@@ -535,7 +534,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
||||
app
|
||||
|
||||
@@ -11,11 +11,12 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::library::{movies_sorting_options, LibraryHandler};
|
||||
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::{
|
||||
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, DELETE_MOVIE_BLOCKS, EDIT_MOVIE_BLOCKS, LIBRARY_BLOCKS,
|
||||
MOVIE_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::test_handler_delegation;
|
||||
@@ -1806,6 +1807,7 @@ mod tests {
|
||||
id: 3,
|
||||
title: "test 1".into(),
|
||||
original_language: Language {
|
||||
id: 1,
|
||||
name: "English".to_owned(),
|
||||
},
|
||||
size_on_disk: 1024,
|
||||
@@ -1822,6 +1824,7 @@ mod tests {
|
||||
id: 2,
|
||||
title: "test 2".into(),
|
||||
original_language: Language {
|
||||
id: 2,
|
||||
name: "Chinese".to_owned(),
|
||||
},
|
||||
size_on_disk: 2048,
|
||||
@@ -1838,6 +1841,7 @@ mod tests {
|
||||
id: 1,
|
||||
title: "test 3".into(),
|
||||
original_language: Language {
|
||||
id: 3,
|
||||
name: "Japanese".to_owned(),
|
||||
},
|
||||
size_on_disk: 512,
|
||||
|
||||
@@ -4,10 +4,11 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
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::{
|
||||
ActiveRadarrBlock, EDIT_MOVIE_SELECTION_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
@@ -47,18 +48,28 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
|
||||
}
|
||||
|
||||
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 {
|
||||
!movie_details_modal.movie_details.is_empty()
|
||||
|| !movie_details_modal.movie_history.is_empty()
|
||||
|| !movie_details_modal.movie_cast.is_empty()
|
||||
|| !movie_details_modal.movie_crew.is_empty()
|
||||
|| !movie_details_modal.movie_releases.is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
!self.app.is_loading && movie_details_modal_is_ready
|
||||
if let Some(movie_details_modal) = &self.app.data.radarr_data.movie_details_modal {
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::MovieDetails => {
|
||||
!self.app.is_loading && !movie_details_modal.movie_details.is_empty()
|
||||
}
|
||||
ActiveRadarrBlock::MovieHistory => {
|
||||
!self.app.is_loading && !movie_details_modal.movie_history.is_empty()
|
||||
}
|
||||
ActiveRadarrBlock::Cast => {
|
||||
!self.app.is_loading && !movie_details_modal.movie_cast.is_empty()
|
||||
}
|
||||
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) {
|
||||
@@ -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![
|
||||
SortOption {
|
||||
name: "Source",
|
||||
@@ -550,6 +561,7 @@ fn releases_sorting_options() -> Vec<SortOption<Release>> {
|
||||
name: "Language",
|
||||
cmp_fn: Some(|a, b| {
|
||||
let default_language_vec = vec![Language {
|
||||
id: 1,
|
||||
name: "_".to_owned(),
|
||||
}];
|
||||
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||
|
||||
@@ -3,6 +3,7 @@ mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::Number;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
@@ -13,11 +14,11 @@ mod tests {
|
||||
releases_sorting_options, MovieDetailsHandler,
|
||||
};
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{
|
||||
Credit, Language, MovieHistoryItem, Quality, QualityWrapper, Release,
|
||||
};
|
||||
use crate::models::radarr_models::RadarrRelease;
|
||||
use crate::models::radarr_models::{Credit, MovieHistoryItem};
|
||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||
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::{HorizontallyScrollableText, ScrollableText};
|
||||
|
||||
@@ -405,7 +406,7 @@ mod tests {
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(simple_stateful_iterable_vec!(
|
||||
Release,
|
||||
RadarrRelease,
|
||||
HorizontallyScrollableText
|
||||
));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
@@ -453,7 +454,7 @@ mod tests {
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(simple_stateful_iterable_vec!(
|
||||
Release,
|
||||
RadarrRelease,
|
||||
HorizontallyScrollableText
|
||||
));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
@@ -996,7 +997,7 @@ mod tests {
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(extended_stateful_iterable_vec!(
|
||||
Release,
|
||||
RadarrRelease,
|
||||
HorizontallyScrollableText
|
||||
));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
@@ -1054,7 +1055,7 @@ mod tests {
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(extended_stateful_iterable_vec!(
|
||||
Release,
|
||||
RadarrRelease,
|
||||
HorizontallyScrollableText
|
||||
));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
@@ -1245,10 +1246,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_manual_search_submit() {
|
||||
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()),
|
||||
..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());
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
@@ -1485,11 +1490,22 @@ mod tests {
|
||||
)]
|
||||
active_radarr_block: ActiveRadarrBlock,
|
||||
) {
|
||||
use crate::models::radarr_models::RadarrRelease;
|
||||
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
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);
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.search.key,
|
||||
@@ -1539,10 +1555,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_sort_key() {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
..MovieDetailsModal::default()
|
||||
});
|
||||
let mut modal = MovieDetailsModal::default();
|
||||
modal.movie_releases.set_items(release_vec());
|
||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.sort.key,
|
||||
@@ -1670,10 +1685,19 @@ mod tests {
|
||||
active_radarr_block: ActiveRadarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
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);
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.update.key,
|
||||
@@ -1733,10 +1757,19 @@ mod tests {
|
||||
active_radarr_block: ActiveRadarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
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);
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.refresh.key,
|
||||
@@ -1829,7 +1862,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1843,7 +1877,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1857,7 +1891,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1871,7 +1906,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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
|
||||
.text
|
||||
.to_lowercase()
|
||||
@@ -1890,7 +1925,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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());
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
@@ -1905,7 +1940,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1919,7 +1955,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 seeder_a = a
|
||||
.seeders
|
||||
@@ -1949,8 +1985,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 {
|
||||
id: 1,
|
||||
name: "_".to_owned(),
|
||||
}];
|
||||
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||
@@ -1971,7 +2008,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1994,15 +2032,39 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_details_handler_is_not_ready_when_loading() {
|
||||
#[rstest]
|
||||
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();
|
||||
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(
|
||||
&DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::MovieDetails,
|
||||
&movie_details_block,
|
||||
&None,
|
||||
);
|
||||
|
||||
@@ -2105,7 +2167,9 @@ mod tests {
|
||||
let mut app = App::default();
|
||||
app.is_loading = false;
|
||||
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);
|
||||
|
||||
let handler = MovieDetailsHandler::with(
|
||||
@@ -2118,8 +2182,8 @@ mod tests {
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
fn release_vec() -> Vec<Release> {
|
||||
let release_a = Release {
|
||||
fn release_vec() -> Vec<RadarrRelease> {
|
||||
let release_a = RadarrRelease {
|
||||
protocol: "Protocol A".to_owned(),
|
||||
age: 1,
|
||||
title: HorizontallyScrollableText::from("Title A"),
|
||||
@@ -2128,6 +2192,7 @@ mod tests {
|
||||
rejected: true,
|
||||
seeders: Some(Number::from(1)),
|
||||
languages: Some(vec![Language {
|
||||
id: 1,
|
||||
name: "Language A".to_owned(),
|
||||
}]),
|
||||
quality: QualityWrapper {
|
||||
@@ -2135,9 +2200,9 @@ mod tests {
|
||||
name: "Quality A".to_owned(),
|
||||
},
|
||||
},
|
||||
..Release::default()
|
||||
..RadarrRelease::default()
|
||||
};
|
||||
let release_b = Release {
|
||||
let release_b = RadarrRelease {
|
||||
protocol: "Protocol B".to_owned(),
|
||||
age: 2,
|
||||
title: HorizontallyScrollableText::from("title B"),
|
||||
@@ -2146,6 +2211,7 @@ mod tests {
|
||||
rejected: false,
|
||||
seeders: Some(Number::from(2)),
|
||||
languages: Some(vec![Language {
|
||||
id: 2,
|
||||
name: "Language B".to_owned(),
|
||||
}]),
|
||||
quality: QualityWrapper {
|
||||
@@ -2153,9 +2219,9 @@ mod tests {
|
||||
name: "Quality B".to_owned(),
|
||||
},
|
||||
},
|
||||
..Release::default()
|
||||
..RadarrRelease::default()
|
||||
};
|
||||
let release_c = Release {
|
||||
let release_c = RadarrRelease {
|
||||
protocol: "Protocol C".to_owned(),
|
||||
age: 3,
|
||||
title: HorizontallyScrollableText::from("Title C"),
|
||||
@@ -2169,13 +2235,13 @@ mod tests {
|
||||
name: "Quality C".to_owned(),
|
||||
},
|
||||
},
|
||||
..Release::default()
|
||||
..RadarrRelease::default()
|
||||
};
|
||||
|
||||
vec![release_a, release_b, release_c]
|
||||
}
|
||||
|
||||
fn sort_options() -> Vec<SortOption<Release>> {
|
||||
fn sort_options() -> Vec<SortOption<RadarrRelease>> {
|
||||
vec![SortOption {
|
||||
name: "Test 1",
|
||||
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
|
||||
|
||||
@@ -8,14 +8,14 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
|
||||
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_models::RootFolder;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
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 super::*;
|
||||
@@ -63,7 +63,7 @@ mod tests {
|
||||
|
||||
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 super::*;
|
||||
|
||||
@@ -8,10 +8,11 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||
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::{
|
||||
ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::QueueEvent;
|
||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
@@ -73,7 +74,7 @@ mod tests {
|
||||
.data
|
||||
.radarr_data
|
||||
.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();
|
||||
|
||||
@@ -101,7 +102,7 @@ mod tests {
|
||||
.data
|
||||
.radarr_data
|
||||
.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();
|
||||
|
||||
@@ -317,7 +318,7 @@ mod tests {
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(extended_stateful_iterable_vec!(Task, String, name));
|
||||
.set_items(extended_stateful_iterable_vec!(RadarrTask, String, name));
|
||||
|
||||
SystemDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.end.key,
|
||||
@@ -356,7 +357,7 @@ mod tests {
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(extended_stateful_iterable_vec!(Task, String, name));
|
||||
.set_items(extended_stateful_iterable_vec!(RadarrTask, String, name));
|
||||
|
||||
SystemDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.end.key,
|
||||
@@ -788,7 +789,11 @@ mod tests {
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(ActiveRadarrBlock::System.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)
|
||||
.handle();
|
||||
|
||||
@@ -9,10 +9,11 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::system::SystemHandler;
|
||||
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::{
|
||||
ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::QueueEvent;
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
@@ -104,7 +105,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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(
|
||||
&DEFAULT_KEYBINDINGS.update.key,
|
||||
@@ -134,7 +139,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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(
|
||||
&DEFAULT_KEYBINDINGS.update.key,
|
||||
@@ -159,7 +168,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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(
|
||||
&DEFAULT_KEYBINDINGS.events.key,
|
||||
@@ -189,7 +202,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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(
|
||||
&DEFAULT_KEYBINDINGS.events.key,
|
||||
@@ -214,7 +231,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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());
|
||||
|
||||
SystemHandler::with(
|
||||
@@ -243,7 +264,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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());
|
||||
|
||||
SystemHandler::with(
|
||||
@@ -270,7 +295,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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(
|
||||
&DEFAULT_KEYBINDINGS.logs.key,
|
||||
@@ -308,7 +337,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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(
|
||||
&DEFAULT_KEYBINDINGS.logs.key,
|
||||
@@ -334,7 +367,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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(
|
||||
&DEFAULT_KEYBINDINGS.tasks.key,
|
||||
@@ -364,7 +401,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.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(
|
||||
&DEFAULT_KEYBINDINGS.tasks.key,
|
||||
@@ -429,7 +470,11 @@ mod tests {
|
||||
fn test_system_handler_is_not_ready_when_logs_is_empty() {
|
||||
let mut app = App::default();
|
||||
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
|
||||
.data
|
||||
.radarr_data
|
||||
@@ -472,7 +517,11 @@ mod tests {
|
||||
let mut app = App::default();
|
||||
app.is_loading = false;
|
||||
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(
|
||||
&DEFAULT_KEYBINDINGS.update.key,
|
||||
@@ -489,7 +538,11 @@ mod tests {
|
||||
let mut app = App::default();
|
||||
app.is_loading = false;
|
||||
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
|
||||
.data
|
||||
.radarr_data
|
||||
|
||||
+39
-76
@@ -1,33 +1,30 @@
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::io::BufReader;
|
||||
use anyhow::Result;
|
||||
use std::panic::PanicHookInfo;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::{io, panic, process};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use app::{log_and_print_error, AppConfig, ServarrConfig};
|
||||
use clap::{
|
||||
command, crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser,
|
||||
};
|
||||
use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser};
|
||||
use clap_complete::generate;
|
||||
use colored::Colorize;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use log::error;
|
||||
use log::{error, warn};
|
||||
use network::NetworkTrait;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
use reqwest::{Certificate, Client};
|
||||
use reqwest::Client;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
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::cli::Command;
|
||||
@@ -64,6 +61,13 @@ mod utils;
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
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(
|
||||
long,
|
||||
global = true,
|
||||
@@ -88,6 +92,7 @@ async fn main() -> Result<()> {
|
||||
} else {
|
||||
confy::load("managarr", "config")?
|
||||
};
|
||||
let spinner_disabled = args.disable_spinner;
|
||||
config.validate();
|
||||
let reqwest_client = build_network_client(&config);
|
||||
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
|
||||
@@ -103,25 +108,24 @@ async fn main() -> Result<()> {
|
||||
|
||||
let app = Arc::new(Mutex::new(App::new(
|
||||
sync_network_tx,
|
||||
config,
|
||||
config.clone(),
|
||||
cancellation_token.clone(),
|
||||
)));
|
||||
|
||||
match args.command {
|
||||
Some(command) => match command {
|
||||
Command::Radarr(_) => {
|
||||
let app_nw = Arc::clone(&app);
|
||||
let mut network = Network::new(&app_nw, cancellation_token, reqwest_client);
|
||||
|
||||
if let Err(e) = cli::handle_command(&app, command, &mut network).await {
|
||||
eprintln!("error: {}", e.to_string().red());
|
||||
process::exit(1);
|
||||
Command::Radarr(_) | Command::Sonarr(_) => {
|
||||
if spinner_disabled {
|
||||
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||
} else {
|
||||
start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||
}
|
||||
}
|
||||
Command::Completions { shell } => {
|
||||
let mut cli = Cli::command();
|
||||
generate(shell, &mut cli, "managarr", &mut io::stdout())
|
||||
}
|
||||
Command::TailLogs { no_color } => tail_logs(no_color).await,
|
||||
},
|
||||
None => {
|
||||
let app_nw = Arc::clone(&app);
|
||||
@@ -144,9 +148,20 @@ async fn start_networking(
|
||||
) {
|
||||
let mut network = Network::new(app, cancellation_token, client);
|
||||
|
||||
while let Some(network_event) = network_rx.recv().await {
|
||||
if let Err(e) = network.handle_network_event(network_event).await {
|
||||
error!("Encountered an error handling network event: {e:?}");
|
||||
loop {
|
||||
select! {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,63 +235,11 @@ fn panic_hook(info: &PanicHookInfo<'_>) {
|
||||
.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 let Some(ref cert_path) = config.radarr.ssl_cert_path {
|
||||
let cert = create_cert(cert_path, "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: &String, servarr_name: &str) -> Certificate {
|
||||
match fs::read(cert_path) {
|
||||
Ok(cert) => match Certificate::from_pem(&cert) {
|
||||
Ok(certificate) => certificate,
|
||||
Err(_) => {
|
||||
log_and_print_error(format!(
|
||||
"Unable to read the specified {} SSL certificate",
|
||||
servarr_name
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
log_and_print_error(format!(
|
||||
"Unable to open specified {} SSL certificate",
|
||||
servarr_name
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn panic_hook(info: &PanicHookInfo<'_>) {
|
||||
use human_panic::{handle_dump, print_msg, Metadata};
|
||||
use human_panic::{handle_dump, metadata, print_msg};
|
||||
|
||||
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 meta = metadata!();
|
||||
let file_path = handle_dump(&meta, info);
|
||||
disable_raw_mode().unwrap();
|
||||
execute!(io::stdout(), LeaveAlternateScreen).unwrap();
|
||||
|
||||
+21
-1
@@ -6,10 +6,15 @@ use radarr_models::RadarrSerdeable;
|
||||
use regex::Regex;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Number;
|
||||
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use sonarr_models::SonarrSerdeable;
|
||||
pub mod radarr_models;
|
||||
pub mod servarr_data;
|
||||
pub mod servarr_models;
|
||||
pub mod sonarr_models;
|
||||
pub mod stateful_list;
|
||||
pub mod stateful_table;
|
||||
pub mod stateful_tree;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "model_tests.rs"]
|
||||
@@ -20,7 +25,7 @@ mod model_tests;
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Route {
|
||||
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
|
||||
Sonarr,
|
||||
Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
|
||||
Readarr,
|
||||
Lidarr,
|
||||
Whisparr,
|
||||
@@ -33,6 +38,11 @@ pub enum Route {
|
||||
#[serde(untagged)]
|
||||
pub enum Serdeable {
|
||||
Radarr(RadarrSerdeable),
|
||||
Sonarr(SonarrSerdeable),
|
||||
}
|
||||
|
||||
pub trait EnumDisplayStyle<'a> {
|
||||
fn to_display_str(self) -> &'a str;
|
||||
}
|
||||
|
||||
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 {
|
||||
Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]")
|
||||
.unwrap()
|
||||
|
||||
@@ -10,6 +10,7 @@ mod tests {
|
||||
use serde::de::IntoDeserializer;
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::models::from_f64;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::{from_i64, strip_non_search_characters};
|
||||
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]
|
||||
fn test_horizontally_scrollable_serialize() {
|
||||
let text = HorizontallyScrollableText::from("Test");
|
||||
|
||||
+29
-279
@@ -9,7 +9,11 @@ use strum_macros::EnumIter;
|
||||
|
||||
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)]
|
||||
#[path = "radarr_models_tests.rs"]
|
||||
@@ -25,7 +29,7 @@ pub struct AddMovieBody {
|
||||
pub minimum_availability: String,
|
||||
pub monitored: bool,
|
||||
pub tags: Vec<i64>,
|
||||
pub add_options: AddOptions,
|
||||
pub add_options: AddMovieOptions,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
@@ -47,54 +51,11 @@ pub struct AddMovieSearchResult {
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddOptions {
|
||||
pub struct AddMovieOptions {
|
||||
pub monitor: String,
|
||||
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)]
|
||||
pub struct BlocklistResponse {
|
||||
pub records: Vec<BlocklistItem>,
|
||||
@@ -123,26 +84,6 @@ pub struct BlocklistItemMovie {
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Collection {
|
||||
@@ -175,12 +116,6 @@ pub struct CollectionMovie {
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Credit {
|
||||
@@ -208,15 +143,6 @@ pub struct DeleteMovieParams {
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadRecord {
|
||||
@@ -253,22 +179,6 @@ pub struct EditCollectionParams {
|
||||
pub search_on_add: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditIndexerParams {
|
||||
pub indexer_id: i64,
|
||||
pub name: Option<String>,
|
||||
pub enable_rss: Option<bool>,
|
||||
pub enable_automatic_search: Option<bool>,
|
||||
pub enable_interactive_search: Option<bool>,
|
||||
pub url: Option<String>,
|
||||
pub api_key: Option<String>,
|
||||
pub seed_ratio: Option<String>,
|
||||
pub tags: Option<Vec<i64>>,
|
||||
pub priority: Option<i64>,
|
||||
pub clear_tags: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditMovieParams {
|
||||
@@ -281,51 +191,6 @@ pub struct EditMovieParams {
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IndexerSettings {
|
||||
@@ -364,28 +229,6 @@ pub struct IndexerValidationFailure {
|
||||
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)]
|
||||
#[derivative(Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -434,8 +277,8 @@ impl Display for MinimumAvailability {
|
||||
}
|
||||
}
|
||||
|
||||
impl MinimumAvailability {
|
||||
pub fn to_display_str<'a>(self) -> &'a str {
|
||||
impl<'a> EnumDisplayStyle<'a> for MinimumAvailability {
|
||||
fn to_display_str(self) -> &'a str {
|
||||
match self {
|
||||
MinimumAvailability::Tba => "TBA",
|
||||
MinimumAvailability::Announced => "Announced",
|
||||
@@ -446,30 +289,30 @@ impl MinimumAvailability {
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum)]
|
||||
pub enum Monitor {
|
||||
pub enum MovieMonitor {
|
||||
#[default]
|
||||
MovieOnly,
|
||||
MovieAndCollection,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Display for Monitor {
|
||||
impl Display for MovieMonitor {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let monitor = match self {
|
||||
Monitor::MovieOnly => "movieOnly",
|
||||
Monitor::MovieAndCollection => "movieAndCollection",
|
||||
Monitor::None => "none",
|
||||
MovieMonitor::MovieOnly => "movieOnly",
|
||||
MovieMonitor::MovieAndCollection => "movieAndCollection",
|
||||
MovieMonitor::None => "none",
|
||||
};
|
||||
write!(f, "{monitor}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Monitor {
|
||||
pub fn to_display_str<'a>(self) -> &'a str {
|
||||
impl<'a> EnumDisplayStyle<'a> for MovieMonitor {
|
||||
fn to_display_str(self) -> &'a str {
|
||||
match self {
|
||||
Monitor::MovieOnly => "Movie only",
|
||||
Monitor::MovieAndCollection => "Movie and Collection",
|
||||
Monitor::None => "None",
|
||||
MovieMonitor::MovieOnly => "Movie only",
|
||||
MovieMonitor::MovieAndCollection => "Movie and Collection",
|
||||
MovieMonitor::None => "None",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,45 +381,6 @@ pub struct MovieHistoryItem {
|
||||
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)]
|
||||
#[derivative(Default)]
|
||||
pub struct Rating {
|
||||
@@ -595,7 +399,7 @@ pub struct RatingsList {
|
||||
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(default)]
|
||||
pub struct Release {
|
||||
pub struct RadarrRelease {
|
||||
pub guid: String,
|
||||
pub protocol: String,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
@@ -616,35 +420,12 @@ pub struct Release {
|
||||
|
||||
#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReleaseDownloadBody {
|
||||
pub struct RadarrReleaseDownloadBody {
|
||||
pub guid: String,
|
||||
pub indexer_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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemStatus {
|
||||
@@ -652,18 +433,11 @@ pub struct SystemStatus {
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Task {
|
||||
pub struct RadarrTask {
|
||||
pub name: String,
|
||||
pub task_name: TaskName,
|
||||
pub task_name: RadarrTaskName,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub interval: i64,
|
||||
pub last_execution: DateTime<Utc>,
|
||||
@@ -673,7 +447,7 @@ pub struct Task {
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub enum TaskName {
|
||||
pub enum RadarrTaskName {
|
||||
#[default]
|
||||
ApplicationCheckUpdate,
|
||||
Backup,
|
||||
@@ -688,7 +462,7 @@ pub enum TaskName {
|
||||
RssSync,
|
||||
}
|
||||
|
||||
impl Display for TaskName {
|
||||
impl Display for RadarrTaskName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let task_name = serde_json::to_string(&self)
|
||||
.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)]
|
||||
#[serde(untagged)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -741,12 +491,12 @@ pub enum RadarrSerdeable {
|
||||
Movies(Vec<Movie>),
|
||||
QualityProfiles(Vec<QualityProfile>),
|
||||
QueueEvents(Vec<QueueEvent>),
|
||||
Releases(Vec<Release>),
|
||||
Releases(Vec<RadarrRelease>),
|
||||
RootFolders(Vec<RootFolder>),
|
||||
SecurityConfig(SecurityConfig),
|
||||
SystemStatus(SystemStatus),
|
||||
Tags(Vec<Tag>),
|
||||
Tasks(Vec<Task>),
|
||||
Tasks(Vec<RadarrTask>),
|
||||
Updates(Vec<Update>),
|
||||
AddMovieSearchResults(Vec<AddMovieSearchResult>),
|
||||
IndexerTestResults(Vec<IndexerTestResult>),
|
||||
@@ -782,12 +532,12 @@ serde_enum_from!(
|
||||
Movies(Vec<Movie>),
|
||||
QualityProfiles(Vec<QualityProfile>),
|
||||
QueueEvents(Vec<QueueEvent>),
|
||||
Releases(Vec<Release>),
|
||||
Releases(Vec<RadarrRelease>),
|
||||
RootFolders(Vec<RootFolder>),
|
||||
SecurityConfig(SecurityConfig),
|
||||
SystemStatus(SystemStatus),
|
||||
Tags(Vec<Tag>),
|
||||
Tasks(Vec<Task>),
|
||||
Tasks(Vec<RadarrTask>),
|
||||
Updates(Vec<Update>),
|
||||
AddMovieSearchResults(Vec<AddMovieSearchResult>),
|
||||
IndexerTestResults(Vec<IndexerTestResult>),
|
||||
|
||||
@@ -5,45 +5,19 @@ mod tests {
|
||||
|
||||
use crate::models::{
|
||||
radarr_models::{
|
||||
AddMovieSearchResult, AuthenticationMethod, AuthenticationRequired, BlocklistItem,
|
||||
BlocklistResponse, CertificateValidation, Collection, Credit, DiskSpace, DownloadRecord,
|
||||
DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
|
||||
MinimumAvailability, Monitor, Movie, MovieHistoryItem, QualityProfile, QueueEvent,
|
||||
RadarrSerdeable, Release, RootFolder, SystemStatus, Tag, Task, TaskName, Update,
|
||||
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, DiskSpace,
|
||||
DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult,
|
||||
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, QualityProfile, RadarrRelease,
|
||||
RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus, Tag, 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]
|
||||
fn test_task_name_display() {
|
||||
assert_str_eq!(
|
||||
TaskName::ApplicationCheckUpdate.to_string(),
|
||||
RadarrTaskName::ApplicationCheckUpdate.to_string(),
|
||||
"ApplicationCheckUpdate"
|
||||
);
|
||||
}
|
||||
@@ -69,22 +43,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_monitor_display() {
|
||||
assert_str_eq!(Monitor::MovieOnly.to_string(), "movieOnly");
|
||||
assert_str_eq!(MovieMonitor::MovieOnly.to_string(), "movieOnly");
|
||||
assert_str_eq!(
|
||||
Monitor::MovieAndCollection.to_string(),
|
||||
MovieMonitor::MovieAndCollection.to_string(),
|
||||
"movieAndCollection"
|
||||
);
|
||||
assert_str_eq!(Monitor::None.to_string(), "none");
|
||||
assert_str_eq!(MovieMonitor::None.to_string(), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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!(
|
||||
Monitor::MovieAndCollection.to_display_str(),
|
||||
MovieMonitor::MovieAndCollection.to_display_str(),
|
||||
"Movie and Collection"
|
||||
);
|
||||
assert_str_eq!(Monitor::None.to_display_str(), "None");
|
||||
assert_str_eq!(MovieMonitor::None.to_display_str(), "None");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -205,6 +179,18 @@ mod tests {
|
||||
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]
|
||||
fn test_radarr_serdeable_from_downloads_response() {
|
||||
let downloads_response = DownloadsResponse {
|
||||
@@ -331,9 +317,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_releases() {
|
||||
let releases = vec![Release {
|
||||
let releases = vec![RadarrRelease {
|
||||
size: 1,
|
||||
..Release::default()
|
||||
..RadarrRelease::default()
|
||||
}];
|
||||
|
||||
let radarr_serdeable: RadarrSerdeable = releases.clone().into();
|
||||
@@ -353,6 +339,21 @@ mod tests {
|
||||
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]
|
||||
fn test_radarr_serdeable_from_system_status() {
|
||||
let system_status = SystemStatus {
|
||||
@@ -382,9 +383,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_tasks() {
|
||||
let tasks = vec![Task {
|
||||
let tasks = vec![RadarrTask {
|
||||
name: "test".to_owned(),
|
||||
..Task::default()
|
||||
..RadarrTask::default()
|
||||
}];
|
||||
|
||||
let radarr_serdeable: RadarrSerdeable = tasks.clone().into();
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod modals;
|
||||
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 crate::models::radarr_models::{
|
||||
Collection, Credit, Indexer, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release,
|
||||
RootFolder,
|
||||
Collection, Credit, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrRelease,
|
||||
};
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
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_table::StatefulTable;
|
||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||
@@ -22,19 +23,7 @@ pub struct MovieDetailsModal {
|
||||
pub movie_history: StatefulTable<MovieHistoryItem>,
|
||||
pub movie_cast: StatefulTable<Credit>,
|
||||
pub movie_crew: StatefulTable<Credit>,
|
||||
pub movie_releases: StatefulTable<Release>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
pub movie_releases: StatefulTable<RadarrRelease>,
|
||||
}
|
||||
|
||||
impl From<&RadarrData<'_>> for EditIndexerModal {
|
||||
@@ -195,7 +184,7 @@ impl From<&RadarrData<'_>> for EditMovieModal {
|
||||
#[derive(Default)]
|
||||
pub struct AddMovieModal {
|
||||
pub root_folder_list: StatefulList<RootFolder>,
|
||||
pub monitor_list: StatefulList<Monitor>,
|
||||
pub monitor_list: StatefulList<MovieMonitor>,
|
||||
pub minimum_availability_list: StatefulList<MinimumAvailability>,
|
||||
pub quality_profile_list: StatefulList<String>,
|
||||
pub tags: HorizontallyScrollableText,
|
||||
@@ -206,7 +195,7 @@ impl From<&RadarrData<'_>> for AddMovieModal {
|
||||
let mut add_movie_modal = AddMovieModal::default();
|
||||
add_movie_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(Monitor::iter()));
|
||||
.set_items(Vec::from_iter(MovieMonitor::iter()));
|
||||
add_movie_modal
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
@@ -291,10 +280,3 @@ impl From<&RadarrData<'_>> for EditCollectionModal {
|
||||
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)]
|
||||
mod test {
|
||||
use crate::models::radarr_models::{
|
||||
Collection, Indexer, IndexerField, MinimumAvailability, Monitor, Movie, RootFolder,
|
||||
};
|
||||
use crate::models::radarr_models::{Collection, MinimumAvailability, Movie, MovieMonitor};
|
||||
use crate::models::servarr_data::radarr::modals::{
|
||||
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::RadarrData;
|
||||
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
@@ -184,7 +183,7 @@ mod test {
|
||||
|
||||
assert_eq!(
|
||||
add_movie_modal.monitor_list.items,
|
||||
Vec::from_iter(Monitor::iter())
|
||||
Vec::from_iter(MovieMonitor::iter())
|
||||
);
|
||||
assert_eq!(
|
||||
add_movie_modal.minimum_availability_list.items,
|
||||
|
||||
@@ -6,13 +6,14 @@ use crate::app::radarr::radarr_context_clues::{
|
||||
SYSTEM_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DiskSpace, DownloadRecord,
|
||||
Indexer, IndexerSettings, Movie, QueueEvent, RootFolder, Task,
|
||||
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord,
|
||||
IndexerSettings, Movie, RadarrTask,
|
||||
};
|
||||
use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem};
|
||||
use crate::models::servarr_data::radarr::modals::{
|
||||
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
|
||||
MovieDetailsModal,
|
||||
AddMovieModal, EditCollectionModal, EditMovieModal, MovieDetailsModal,
|
||||
};
|
||||
use crate::models::servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder};
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{
|
||||
@@ -47,7 +48,7 @@ pub struct RadarrData<'a> {
|
||||
pub collection_movies: StatefulTable<CollectionMovie>,
|
||||
pub logs: StatefulList<HorizontallyScrollableText>,
|
||||
pub log_details: StatefulList<HorizontallyScrollableText>,
|
||||
pub tasks: StatefulTable<Task>,
|
||||
pub tasks: StatefulTable<RadarrTask>,
|
||||
pub queued_events: StatefulTable<QueueEvent>,
|
||||
pub updates: ScrollableText,
|
||||
pub main_tabs: TabState,
|
||||
|
||||
@@ -19,6 +19,14 @@ mod tests {
|
||||
use crate::assert_movie_info_tabs_reset;
|
||||
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]
|
||||
fn test_from_tuple_to_route_with_context() {
|
||||
assert_eq!(
|
||||
@@ -60,7 +68,7 @@ mod tests {
|
||||
assert_eq!(radarr_data.disk_space_vec, Vec::new());
|
||||
assert!(radarr_data.version.is_empty());
|
||||
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!(radarr_data.downloads.items.is_empty());
|
||||
assert!(radarr_data.indexers.items.is_empty());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(test)]
|
||||
pub mod utils {
|
||||
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::radarr_data::RadarrData;
|
||||
@@ -24,7 +24,7 @@ pub mod utils {
|
||||
.set_items(vec![Credit::default()]);
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(vec![Release::default()]);
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
|
||||
let mut radarr_data = RadarrData {
|
||||
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 serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use sonarr_network::SonarrEvent;
|
||||
use strum_macros::Display;
|
||||
use tokio::select;
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::{App, ServarrConfig};
|
||||
use crate::models::Serdeable;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
|
||||
pub mod radarr_network;
|
||||
pub mod sonarr_network;
|
||||
mod utils;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "network_tests.rs"]
|
||||
mod network_tests;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum NetworkEvent {
|
||||
Radarr(RadarrEvent),
|
||||
}
|
||||
|
||||
#[cfg_attr(test, automock)]
|
||||
#[async_trait]
|
||||
pub trait NetworkTrait {
|
||||
async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result<Serdeable>;
|
||||
}
|
||||
|
||||
pub trait NetworkResource {
|
||||
fn resource(&self) -> &'static str;
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum NetworkEvent {
|
||||
Radarr(RadarrEvent),
|
||||
Sonarr(SonarrEvent),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Network<'a, 'b> {
|
||||
client: Client,
|
||||
cancellation_token: CancellationToken,
|
||||
pub cancellation_token: CancellationToken,
|
||||
pub app: &'a Arc<Mutex<App<'b>>>,
|
||||
}
|
||||
|
||||
@@ -52,6 +59,10 @@ impl<'a, 'b> NetworkTrait for Network<'a, 'b> {
|
||||
.handle_radarr_event(radarr_event)
|
||||
.await
|
||||
.map(Serdeable::from),
|
||||
NetworkEvent::Sonarr(sonarr_event) => self
|
||||
.handle_sonarr_event(sonarr_event)
|
||||
.await
|
||||
.map(Serdeable::from),
|
||||
};
|
||||
|
||||
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>(
|
||||
&mut self,
|
||||
request_props: RequestProps<B>,
|
||||
@@ -89,9 +104,6 @@ impl<'a, 'b> Network<'a, 'b> {
|
||||
select! {
|
||||
_ = self.cancellation_token.cancelled() => {
|
||||
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())
|
||||
}
|
||||
resp = self.call_api(request_props).await.send() => {
|
||||
@@ -179,6 +191,71 @@ impl<'a, 'b> Network<'a, 'b> {
|
||||
.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)]
|
||||
|
||||
@@ -12,9 +12,11 @@ mod tests {
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::app::{App, AppConfig, RadarrConfig};
|
||||
use crate::app::{App, AppConfig, ServarrConfig};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::NetworkResource;
|
||||
use crate::network::{Network, NetworkEvent, NetworkTrait, RequestMethod, RequestProps};
|
||||
|
||||
#[tokio::test]
|
||||
@@ -34,14 +36,14 @@ mod tests {
|
||||
);
|
||||
let mut app = App::default();
|
||||
app.is_loading = true;
|
||||
let radarr_config = RadarrConfig {
|
||||
let radarr_config = ServarrConfig {
|
||||
host,
|
||||
api_token: String::new(),
|
||||
port,
|
||||
ssl_cert_path: None,
|
||||
..RadarrConfig::default()
|
||||
..ServarrConfig::default()
|
||||
};
|
||||
app.config.radarr = radarr_config;
|
||||
app.config.radarr = Some(radarr_config);
|
||||
let app_arc = Arc::new(Mutex::new(app));
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
@@ -181,11 +183,31 @@ mod tests {
|
||||
|
||||
assert!(!async_server.matched_async().await);
|
||||
assert!(app_arc.lock().await.error.text.is_empty());
|
||||
assert!(!network.cancellation_token.is_cancelled());
|
||||
assert!(resp.is_ok());
|
||||
assert_eq!(resp.unwrap(), Test::default());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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]
|
||||
async fn test_handle_request_get_invalid_body() {
|
||||
let mut server = Server::new_async().await;
|
||||
@@ -375,6 +397,256 @@ mod tests {
|
||||
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)]
|
||||
struct Test {
|
||||
pub value: String,
|
||||
@@ -405,3 +677,78 @@ mod tests {
|
||||
(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
-406
File diff suppressed because it is too large
Load Diff
+579
-526
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::{
|
||||
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::Route;
|
||||
use crate::models::{EnumDisplayStyle, Route};
|
||||
use crate::ui::radarr_ui::collections::draw_collections;
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::utils::{
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::models::servarr_data::radarr::modals::EditCollectionModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::Route;
|
||||
use crate::models::{EnumDisplayStyle, Route};
|
||||
use crate::render_selectable_input_box;
|
||||
use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi;
|
||||
use crate::ui::radarr_ui::collections::draw_collections;
|
||||
|
||||
@@ -5,8 +5,8 @@ use ratatui::widgets::{Cell, Row};
|
||||
use ratatui::Frame;
|
||||
|
||||
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_models::Indexer;
|
||||
use crate::models::Route;
|
||||
use crate::ui::radarr_ui::indexers::edit_indexer_ui::EditIndexerUi;
|
||||
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::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::Route;
|
||||
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::servarr_data::radarr::modals::AddMovieModal;
|
||||
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::library::draw_library;
|
||||
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::{
|
||||
ActiveRadarrBlock, EDIT_MOVIE_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::Route;
|
||||
use crate::models::{EnumDisplayStyle, Route};
|
||||
use crate::render_selectable_input_box;
|
||||
use crate::ui::radarr_ui::library::draw_library;
|
||||
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 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::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
|
||||
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) {
|
||||
let block = layout_block_top_border();
|
||||
let unknown_download_status = "Status: Unknown".to_owned();
|
||||
|
||||
match app.data.radarr_data.movie_details_modal.as_ref() {
|
||||
Some(movie_details_modal) if !app.is_loading => {
|
||||
@@ -182,7 +183,7 @@ fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
||||
.items
|
||||
.iter()
|
||||
.find(|&line| line.starts_with("Status: "))
|
||||
.unwrap()
|
||||
.unwrap_or(&unknown_download_status)
|
||||
.split(": ")
|
||||
.collect::<Vec<&str>>()[1];
|
||||
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) {
|
||||
let cast_row_mapping = |cast_member: &Credit| {
|
||||
let Credit {
|
||||
person_name,
|
||||
character,
|
||||
..
|
||||
} = cast_member;
|
||||
match app.data.radarr_data.movie_details_modal.as_mut() {
|
||||
Some(movie_details_modal) if !app.is_loading => {
|
||||
let cast_row_mapping = |cast_member: &Credit| {
|
||||
let Credit {
|
||||
person_name,
|
||||
character,
|
||||
..
|
||||
} = cast_member;
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(person_name.to_owned()),
|
||||
Cell::from(character.clone().unwrap_or_default()),
|
||||
])
|
||||
.success()
|
||||
};
|
||||
let content = Some(
|
||||
&mut app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.movie_cast,
|
||||
);
|
||||
let help_footer = app
|
||||
.data
|
||||
.radarr_data
|
||||
.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)]);
|
||||
Row::new(vec![
|
||||
Cell::from(person_name.to_owned()),
|
||||
Cell::from(character.clone().unwrap_or_default()),
|
||||
])
|
||||
.success()
|
||||
};
|
||||
let content = Some(&mut movie_details_modal.movie_cast);
|
||||
let help_footer = app
|
||||
.data
|
||||
.radarr_data
|
||||
.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) {
|
||||
let crew_row_mapping = |crew_member: &Credit| {
|
||||
let Credit {
|
||||
person_name,
|
||||
job,
|
||||
department,
|
||||
..
|
||||
} = crew_member;
|
||||
match app.data.radarr_data.movie_details_modal.as_mut() {
|
||||
Some(movie_details_modal) if !app.is_loading => {
|
||||
let crew_row_mapping = |crew_member: &Credit| {
|
||||
let Credit {
|
||||
person_name,
|
||||
job,
|
||||
department,
|
||||
..
|
||||
} = crew_member;
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(person_name.to_owned()),
|
||||
Cell::from(job.clone().unwrap_or_default()),
|
||||
Cell::from(department.clone().unwrap_or_default()),
|
||||
])
|
||||
.success()
|
||||
};
|
||||
let content = Some(
|
||||
&mut app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.movie_crew,
|
||||
);
|
||||
let help_footer = app
|
||||
.data
|
||||
.radarr_data
|
||||
.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);
|
||||
Row::new(vec![
|
||||
Cell::from(person_name.to_owned()),
|
||||
Cell::from(job.clone().unwrap_or_default()),
|
||||
Cell::from(department.clone().unwrap_or_default()),
|
||||
])
|
||||
.success()
|
||||
};
|
||||
let content = Some(&mut movie_details_modal.movie_crew);
|
||||
let help_footer = app
|
||||
.data
|
||||
.radarr_data
|
||||
.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) {
|
||||
@@ -372,7 +380,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
.clone(),
|
||||
movie_details_modal.movie_releases.items.is_empty(),
|
||||
),
|
||||
_ => (Release::default(), true),
|
||||
_ => (RadarrRelease::default(), true),
|
||||
};
|
||||
let current_route = *app.get_current_route();
|
||||
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)
|
||||
.movie_releases,
|
||||
);
|
||||
let releases_row_mapping = |release: &Release| {
|
||||
let Release {
|
||||
let releases_row_mapping = |release: &RadarrRelease| {
|
||||
let RadarrRelease {
|
||||
protocol,
|
||||
age,
|
||||
title,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user