Compare commits

..

40 Commits

Author SHA1 Message Date
48ad17c6f1 style: Updated the contributing doc to also explain how to install commitizen 2024-11-06 16:48:27 -07:00
3cd15f34cd style: Test install for commitizen 2024-11-06 16:39:26 -07:00
53ca14e64d fix(handler): Fixed a bug in the movie details handler that would allow key events to be processed before the data was finished loading 2024-11-06 16:17:23 -07:00
0d8803d35d fix(ui): Fixed a bug that would freeze all user input while background network requests were running 2024-11-06 15:50:47 -07:00
8c90221a81 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 2024-11-06 14:52:48 -07:00
a708f71d57 fix(radarr_ui): Fixed a race condition bug in the movie details UI that would panic if the user changes tabs too quickly 2024-11-06 11:29:49 -07:00
2a13f74a2b Updated the release workflows to use the correct flags and commands 2024-11-05 18:33:17 -07:00
2a97c49a8e Updated workflows to allow manual releases [skip ci] 2024-11-05 18:29:45 -07:00
8c155ce656 Updated release actions to only be executable by the repository owner [skip ci] 2024-11-05 18:25:26 -07:00
Alex Clarke
5245ba6d98 Merge pull request #14 from Dark-Alex-17/release-plz-2024-11-06T01-20-32Z
chore: release v0.2.1
2024-11-05 18:24:50 -07:00
github-actions[bot]
f9789ecc9b chore: release v0.2.1 2024-11-06 01:20:33 +00:00
9936ce1ab5 Removed the need for use_ssl to indicate SSL usage; instead just use the ssl_cert_path
Added the ability to specify either host/port, or uri for configuring Radarr
2024-11-05 18:16:01 -07:00
650c9783a6 Applied bug fix to the downloads tab as well as the context [skip ci] 2024-11-04 14:21:34 -07:00
b253a389eb Updated the README to not include the GitHub downloads badge since all binary releases are on crates.io [skip ci] 2024-11-04 10:12:45 -07:00
5023fbd3d1 Set all releases as manually triggered instead of automatic [skip ci] 2024-11-04 10:10:36 -07:00
fdb08fbd34 Added additional workflows for releasing minor and major releases, in addition to just patches so I can manually trigger them and update the Changelog dynamically. [skip ci] 2024-11-03 16:37:52 -07:00
b125d3341a Updated dockerfile to no longer use the --disable-terminal-size-checks flag [skip ci] 2024-11-03 16:20:32 -07:00
Alex Clarke
f73e3a4817 Merge pull request #11 from Dark-Alex-17/rc/v0.2.0-address-community-comments
Rc/v0.2.0 address community comments
2024-11-03 16:12:29 -07:00
1ff31b1bd9 Updated codecov config to also ignore the ui directory entirely, since it's not very conducive to tests as it is 2024-11-03 16:05:17 -07:00
ce4cbd8652 Updated codecov config to ignore test files 2024-11-03 16:01:49 -07:00
346d95f8ec Added Codecov config 2024-11-03 15:57:18 -07:00
85ea05e3c8 Incremented minor release since removing the --disable-terminal-size-checks flag is a breaking change, but the application is not yet ready for a 1.0 release. 2024-11-03 15:42:59 -07:00
93d78701ce fix:fixed divide by zero panic when download size is unknown 2024-11-03 15:36:26 -07:00
8d7cb63c7a Remove the terminal size checks since they've caused so many issues since their introduction 2024-11-03 15:33:08 -07:00
c8c7d00517 Added environment variables section to the README for added visibility into the feature 2024-11-03 15:06:24 -07:00
Alex Clarke
9402ad3f3b Merge pull request #9 from tangowithfoxtrot/add-env-var-config-options
Add environment variables for --config and terminal size check args
2024-11-03 14:43:50 -07:00
tangowithfoxtrot
ea9a9070ce Merge branch 'rc/v0.1.6-address-community-comments' into add-env-var-config-options 2024-11-03 13:40:53 -08:00
a0fe51c57b Added help that's always visible for modals with new shortcuts for accepting all modals, or closing all modals without the need of seeing the UI 2024-11-03 14:25:33 -07:00
tangowithfoxtrot
9326428141 feat: allow configuration via env vars 2024-11-03 11:20:15 -08:00
Alex Clarke
c1da8592b4 Merge pull request #7 from Dark-Alex-17/release-plz-2024-11-03T00-33-10Z
chore: release v0.1.5
2024-11-02 18:37:24 -06:00
github-actions[bot]
aa43219c29 chore: release v0.1.5 2024-11-03 00:33:11 +00:00
f6f477b124 Added HTTPS support for all Servarrs 2024-11-02 18:32:44 -06:00
Alex Clarke
76f22e7434 Merge pull request #5 from Dark-Alex-17/release-plz-2024-11-01T19-06-52Z
chore: release v0.1.4
2024-11-01 13:13:03 -06:00
github-actions[bot]
28aad8cd14 chore: release v0.1.4 2024-11-01 19:06:53 +00:00
97c8f8fc49 Added the ability to fetch host configs and security configs to the CLI 2024-11-01 13:02:39 -06:00
9da4ebfe11 Updated README to be more clear about what features are supported [skip ci] 2024-10-31 15:53:08 -06:00
9961fe2f82 Hotfix for demo; added the --config flag to specify different config files, and added --disable-terminal-size-checks for running the demo and for users who really know what they're doing 2024-10-31 15:04:36 -06:00
61ce0468c6 Added --config and --disable-terminal-size-checks flags to make the demo work properly [skip ci] 2024-10-31 14:53:49 -06:00
Alex Clarke
29071b11da Create FUNDING.yml
[skip ci] Added funding information to the repo
2024-10-30 17:05:25 -06:00
Alex Clarke
f2129ba321 Merge pull request #4 from Dark-Alex-17/release-plz-2024-10-30T22-44-04Z
chore: release v0.1.2
2024-10-30 16:51:51 -06:00
77 changed files with 1964 additions and 449 deletions
+7
View File
@@ -0,0 +1,7 @@
[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
+1
View File
@@ -0,0 +1 @@
github: Dark-Alex-17
@@ -1,16 +1,14 @@
# 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 Release PR and publish to crates.io
name: Create major release
permissions:
pull-requests: write
contents: write
on:
push:
branches:
- main
workflow_dispatch:
jobs:
release-plz:
@@ -19,6 +17,12 @@ jobs:
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:
@@ -27,6 +31,8 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
with:
command: release --bump-major
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+38
View File
@@ -0,0 +1,38 @@
# 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:
command: release --bump-minor
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+38
View File
@@ -0,0 +1,38 @@
# 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:
command: release --bump-patch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+8
View File
@@ -0,0 +1,8 @@
repos:
- hooks:
- id: commitizen
- id: commitizen-branch
stages:
- pre-push
repo: https://github.com/commitizen-tools/commitizen
rev: v3.30.0
+23
View File
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.1](https://github.com/Dark-Alex-17/managarr/compare/v0.2.0...v0.2.1) - 2024-11-06
### Other
- Removed the need for use_ssl to indicate SSL usage; instead just use the ssl_cert_path
- Applied bug fix to the downloads tab as well as the context [skip ci]
- Updated the README to not include the GitHub downloads badge since all binary releases are on crates.io [skip ci]
- Set all releases as manually triggered instead of automatic [skip ci]
- Updated dockerfile to no longer use the --disable-terminal-size-checks flag [skip ci]
## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03
### Other
- Added HTTPS support for all Servarrs
## [0.1.4](https://github.com/Dark-Alex-17/managarr/compare/v0.1.3...v0.1.4) - 2024-11-01
### Other
- Added the ability to fetch host configs and security configs to the CLI
- Updated README to be more clear about what features are supported [skip ci]
## [0.1.2](https://github.com/Dark-Alex-17/managarr/compare/v0.1.1...v0.1.2) - 2024-10-30
### Other
+32
View File
@@ -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
+1 -1
View File
@@ -1148,7 +1148,7 @@ dependencies = [
[[package]]
name = "managarr"
version = "0.1.2"
version = "0.2.1"
dependencies = [
"anyhow",
"assert_cmd",
+3 -3
View File
@@ -1,9 +1,9 @@
[package]
name = "managarr"
version = "0.1.2"
version = "0.2.1"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"]
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
documentation = "https://github.com/Dark-Alex-17/managarr"
repository = "https://github.com/Dark-Alex-17/managarr"
homepage = "https://github.com/Dark-Alex-17/managarr"
@@ -38,7 +38,7 @@ tokio = { version = "1.36.0", features = ["full"] }
tokio-util = "0.7.8"
ratatui = { version = "0.28.0", features = ["all-widgets"] }
urlencoding = "2.1.2"
clap = { version = "4.5.20", features = ["derive", "cargo"] }
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
clap_complete = "4.5.33"
itertools = "0.13.0"
ctrlc = "3.4.5"
+45 -26
View File
@@ -2,13 +2,11 @@
![check](https://github.com/Dark-Alex-17/managarr/actions/workflows/check.yml/badge.svg)
![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/test.yml/badge.svg)
![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/release.yml/badge.svg)
![License](https://img.shields.io/badge/license-MIT-blueviolet.svg)
![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code)
[![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr)
![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff)
[![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr)
[![GitHub Downloads](https://img.shields.io/github/downloads/Dark-Alex-17/managarr/total.svg?label=GitHub%20downloads)](https://github.com/Dark-Alex-17/managarr/releases)
![Crate.io downloads](https://img.shields.io/crates/d/managarr?label=Crate%20downloads)
Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust!
@@ -17,14 +15,14 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
## What Servarrs are supported?
- ![radarr_logo](logos/radarr.png) [Radarr](https://wiki.servarr.com/radarr)
- ![sonarr_logo](logos/sonarr.png) [Sonarr](https://wiki.servarr.com/en/sonarr)
- ![readarr_logo](logos/readarr.png) [Readarr](https://wiki.servarr.com/en/readarr)
- ![lidarr_logo](logos/lidarr.png) [Lidarr](https://wiki.servarr.com/en/lidarr)
- ![prowlarr_logo](logos/prowlarr.png) [Prowlarr](https://wiki.servarr.com/en/prowlarr)
- ![whisparr_logo](logos/whisparr.png) [Whisparr](https://wiki.servarr.com/whisparr)
- ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/)
- ![tautulli_logo](logos/tautulli.png) [Tautulli](https://tautulli.com/)
- [x] ![radarr_logo](logos/radarr.png) [Radarr](https://wiki.servarr.com/radarr)
- [ ] ![sonarr_logo](logos/sonarr.png) [Sonarr](https://wiki.servarr.com/en/sonarr)
- [ ] ![readarr_logo](logos/readarr.png) [Readarr](https://wiki.servarr.com/en/readarr)
- [ ] ![lidarr_logo](logos/lidarr.png) [Lidarr](https://wiki.servarr.com/en/lidarr)
- [ ] ![prowlarr_logo](logos/prowlarr.png) [Prowlarr](https://wiki.servarr.com/en/prowlarr)
- [ ] ![whisparr_logo](logos/whisparr.png) [Whisparr](https://wiki.servarr.com/whisparr)
- [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/)
- [ ] ![tautulli_logo](logos/tautulli.png) [Tautulli](https://tautulli.com/)
## Try Before You Buy
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
@@ -60,6 +58,7 @@ You can also clone this repo and run `make docker` to build a docker image local
- [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
@@ -103,7 +102,8 @@ You can also clone this repo and run `make docker` to build a docker image local
### The Managarr CLI
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
All management features available in the TUI are also available in the CLI.
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.
@@ -111,12 +111,12 @@ To see all available commands, simply run `managarr --help`:
```shell
$ managarr --help
managarr 0.0.36
managarr 0.2.1
Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs
Usage: managarr [COMMAND]
Usage: managarr [OPTIONS] [COMMAND]
Commands:
radarr Commands for manging your Radarr instance
@@ -124,8 +124,9 @@ Commands:
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
--config <CONFIG> The Managarr configuration file to use
-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:
@@ -134,7 +135,7 @@ All subcommands also have detailed help menus to show you how to use them. For e
$ managarr radarr --help
Commands for manging your Radarr instance
Usage: managarr radarr <COMMAND>
Usage: managarr radarr [OPTIONS] <COMMAND>
Commands:
add Commands to add or create new resources within your Radarr instance
@@ -154,7 +155,8 @@ Commands:
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
--config <CONFIG> The Managarr configuration file to use
-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:
@@ -185,42 +187,59 @@ $HOME/Library/Application Support/managarr/config.yml
%APPDATA%/Roaming/managarr/config.yml
```
## Specify Which Configuration File to Use
It can sometimes be useful to specify the configuration file you wish to use. This is useful in cases
where you may have more than one instance of a given Servarr running. Thus, you can specify the
config file using the `--config` flag:
```shell
managarr --config /path/to/config.yml
```
### Example Configuration:
```yaml
radarr:
host: 127.0.0.1
host: 192.168.0.78
port: 7878
api_token: someApiToken1234567890
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
sonarr:
host: 127.0.0.1
port: 8989
uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
api_token: someApiToken1234567890
readarr:
host: 127.0.0.1
host: 192.168.0.87
port: 8787
api_token: someApiToken1234567890
lidarr:
host: 127.0.0.1
host: 192.168.0.86
port: 8686
api_token: someApiToken1234567890
whisparr:
host: 127.0.0.1
host: 192.168.0.69
port: 6969
api_token: someApiToken1234567890
ssl_cert_path: /path/to/whisparr.crt
bazarr:
host: 127.0.0.1
host: 192.168.0.67
port: 6767
api_token: someApiToken1234567890
prowlarr:
host: 127.0.0.1
host: 192.168.0.96
port: 9696
api_token: someApiToken1234567890
tautulli:
host: 127.0.0.1
host: 192.168.0.81
port: 8181
api_token: someApiToken1234567890
```
## 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` |
## 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)
with all items tagged `Beta`.
+6
View File
@@ -0,0 +1,6 @@
coverage:
range: "80..100"
ignore:
- "**/*_tests.rs"
- "src/ui"
+6
View File
@@ -0,0 +1,6 @@
{
"name": "managarr",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+1
View File
@@ -0,0 +1 @@
{}
+39 -7
View File
@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use pretty_assertions::{assert_eq, assert_str_eq};
use pretty_assertions::assert_eq;
use tokio::sync::mpsc;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
@@ -87,7 +87,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 +100,8 @@ mod tests {
assert!(!app.cancellation_token.is_cancelled());
assert!(!new_token.is_cancelled());
assert!(!app.is_loading);
assert!(app.should_refresh);
}
#[test]
@@ -145,6 +151,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 +187,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()
@@ -170,6 +200,10 @@ mod tests {
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetRootFolders.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
@@ -182,10 +216,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);
@@ -221,8 +251,10 @@ mod tests {
fn test_radarr_config_default() {
let radarr_config = RadarrConfig::default();
assert_str_eq!(radarr_config.host, "localhost");
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);
}
}
+5
View File
@@ -33,6 +33,7 @@ generate_keybindings! {
tab,
delete,
submit,
confirm,
quit,
esc
}
@@ -140,6 +141,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
key: Key::Enter,
desc: "submit",
},
confirm: KeyBinding {
key: Key::Ctrl('s'),
desc: "submit",
},
quit: KeyBinding {
key: Key::Char('q'),
desc: "quit",
+1
View File
@@ -31,6 +31,7 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.tab, Key::Tab, "tab")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")]
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")]
#[case(DEFAULT_KEYBINDINGS.quit, Key::Char('q'), "quit")]
#[case(DEFAULT_KEYBINDINGS.esc, Key::Esc, "close")]
fn test_default_key_bindings_and_descriptions(
+39 -3
View File
@@ -1,4 +1,7 @@
use std::process;
use anyhow::anyhow;
use colored::Colorize;
use log::{debug, error};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::Sender;
@@ -53,7 +56,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;
@@ -110,6 +116,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()
}
@@ -166,24 +174,52 @@ pub struct Data<'a> {
pub radarr_data: RadarrData<'a>,
}
pub trait ServarrConfig {
fn validate(&self);
}
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct AppConfig {
pub radarr: RadarrConfig,
}
impl ServarrConfig for AppConfig {
fn validate(&self) {
self.radarr.validate();
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RadarrConfig {
pub host: String,
pub host: Option<String>,
pub port: Option<u16>,
pub uri: Option<String>,
pub api_token: String,
pub ssl_cert_path: Option<String>,
}
impl ServarrConfig for RadarrConfig {
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());
process::exit(1);
}
}
}
impl Default for RadarrConfig {
fn default() -> Self {
RadarrConfig {
host: "localhost".to_string(),
host: Some("localhost".to_string()),
port: Some(7878),
uri: None,
api_token: "".to_string(),
ssl_cert_path: None,
}
}
}
pub fn log_and_print_error(error: String) {
error!("{}", error);
eprintln!("error: {}", error.red());
}
+12 -19
View File
@@ -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::GetOverview.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetStatus.into())
.await;
}
async fn populate_movie_collection_table(&mut self) {
+5
View File
@@ -120,6 +120,11 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.confirm, "submit"),
(DEFAULT_KEYBINDINGS.esc, "cancel"),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
+18 -2
View File
@@ -5,8 +5,8 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
};
@@ -349,6 +349,22 @@ mod tests {
assert_eq!(add_movie_search_results_context_clues_iter.next(), None);
}
#[test]
fn test_confirmation_prompt_context_clues() {
let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter();
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm);
assert_str_eq!(*description, "submit");
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel");
assert_eq!(confirmation_prompt_context_clues_iter.next(), None);
}
#[test]
fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
+14 -30
View File
@@ -508,6 +508,14 @@ mod tests {
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetStatus.into()
);
assert!(app.is_loading);
}
@@ -529,6 +537,10 @@ mod tests {
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetRootFolders.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
@@ -537,10 +549,6 @@ mod tests {
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);
}
+10
View File
@@ -21,6 +21,8 @@ mod get_command_handler_tests;
pub enum RadarrGetCommand {
#[command(about = "Get the shared settings for all indexers")]
AllIndexerSettings,
#[command(about = "Fetch the host config for your Radarr instance")]
HostConfig,
#[command(about = "Get detailed information for the movie with the given ID")]
MovieDetails {
#[arg(
@@ -39,6 +41,8 @@ pub enum RadarrGetCommand {
)]
movie_id: i64,
},
#[command(about = "Fetch the security config for your Radarr instance")]
SecurityConfig,
#[command(about = "Get the system status")]
SystemStatus,
}
@@ -73,12 +77,18 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan
RadarrGetCommand::AllIndexerSettings => {
execute_network_event!(self, RadarrEvent::GetAllIndexerSettings);
}
RadarrGetCommand::HostConfig => {
execute_network_event!(self, RadarrEvent::GetHostConfig);
}
RadarrGetCommand::MovieDetails { movie_id } => {
execute_network_event!(self, RadarrEvent::GetMovieDetails(Some(movie_id)));
}
RadarrGetCommand::MovieHistory { movie_id } => {
execute_network_event!(self, RadarrEvent::GetMovieHistory(Some(movie_id)));
}
RadarrGetCommand::SecurityConfig => {
execute_network_event!(self, RadarrEvent::GetSecurityConfig);
}
RadarrGetCommand::SystemStatus => {
execute_network_event!(self, RadarrEvent::GetStatus);
}
@@ -29,6 +29,14 @@ mod test {
assert!(result.is_ok());
}
#[test]
fn test_get_host_config_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "host-config"]);
assert!(result.is_ok());
}
#[test]
fn test_movie_details_requires_movie_id() {
let result =
@@ -81,6 +89,14 @@ mod test {
assert!(result.is_ok());
}
#[test]
fn test_get_security_config_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "security-config"]);
assert!(result.is_ok());
}
#[test]
fn test_system_status_has_no_arg_requirements() {
let result =
@@ -135,6 +151,29 @@ mod test {
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>(RadarrEvent::GetHostConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_host_config_command = RadarrGetCommand::HostConfig;
let result =
RadarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_movie_details_command() {
let expected_movie_id = 1;
@@ -187,6 +226,29 @@ mod test {
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>(RadarrEvent::GetSecurityConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_security_config_command = RadarrGetCommand::SecurityConfig;
let result =
RadarrGetCommandHandler::with(&app_arc, get_security_config_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();
@@ -584,6 +584,9 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::network::radarr_network::RadarrEvent;
use super::*;
@@ -716,6 +719,43 @@ mod tests {
assert!(app.data.radarr_data.blocklist.sort.is_none());
assert!(!app.data.radarr_data.blocklist.sort_asc);
}
#[rstest]
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
RadarrEvent::DeleteBlocklistItem(None)
)]
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::BlocklistClearAllItemsPrompt,
RadarrEvent::ClearBlocklist
)]
fn test_blocklist_prompt_confirm(
#[case] base_route: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent,
) {
let mut app = App::default();
app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&prompt_block,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
}
}
#[test]
+20 -2
View File
@@ -182,8 +182,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::Blocklist {
match self.key {
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
self.app.should_refresh = true;
}
@@ -204,7 +204,25 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
}
_ => (),
},
ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteBlocklistItem(None));
self.app.pop_navigation_stack();
}
}
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -1040,6 +1040,7 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_COLLECTION_SELECTION_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
use crate::test_edit_collection_key;
use super::*;
@@ -1509,6 +1510,36 @@ mod tests {
assert!(app.data.radarr_data.collections.sort.is_none());
assert!(!app.data.radarr_data.collections.sort_asc);
}
#[test]
fn test_update_all_collections_prompt_confirm_confirm() {
let mut app = App::default();
app
.data
.radarr_data
.collections
.set_items(vec![Collection::default()]);
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
CollectionsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::UpdateAllCollectionsPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::UpdateCollections)
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
);
}
}
#[rstest]
@@ -291,19 +291,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput {
handle_text_box_keys!(
self,
key,
self
.app
.data
.radarr_data
.edit_collection_modal
.as_mut()
.unwrap()
.path
)
match self.active_radarr_block {
ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.radarr_data
.edit_collection_modal
.as_mut()
.unwrap()
.path
)
}
ActiveRadarrBlock::EditCollectionPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditCollectionConfirmPrompt
&& *key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -871,7 +871,15 @@ mod tests {
mod test_handle_key_char {
use super::*;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::{
models::{
servarr_data::radarr::{
modals::EditCollectionModal, radarr_data::EDIT_COLLECTION_SELECTION_BLOCKS,
},
BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
#[test]
fn test_edit_collection_root_folder_path_input_backspace() {
@@ -927,6 +935,39 @@ mod tests {
"h"
);
}
#[test]
fn test_edit_collection_confirm_prompt_prompt_confirmation_confirm() {
let mut app = App::default();
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditCollection(None))
);
assert!(app.should_refresh);
}
}
#[test]
@@ -385,6 +385,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
.unwrap()
)
}
ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -349,6 +349,9 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::network::radarr_network::RadarrEvent;
use super::*;
@@ -450,6 +453,47 @@ mod tests {
);
assert!(!app.should_refresh);
}
#[rstest]
#[case(
ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::DeleteDownloadPrompt,
RadarrEvent::DeleteDownload(None)
)]
#[case(
ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::UpdateDownloadsPrompt,
RadarrEvent::UpdateDownloads
)]
fn test_downloads_prompt_confirm_submit(
#[case] base_route: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent,
) {
let mut app = App::default();
app
.data
.radarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&prompt_block,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
}
}
#[test]
+19 -2
View File
@@ -119,8 +119,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
match self.key {
match self.active_radarr_block {
ActiveRadarrBlock::Downloads => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.update.key => {
self
.app
@@ -130,7 +130,24 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::DeleteDownloadPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None));
self.app.pop_navigation_stack();
}
}
ActiveRadarrBlock::UpdateDownloadsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -429,6 +429,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
.tags
);
}
ActiveRadarrBlock::EditIndexerPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditIndexerConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -1282,6 +1282,9 @@ mod tests {
mod test_handle_key_char {
use crate::app::App;
use crate::models::servarr_data::radarr::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;
use pretty_assertions::assert_str_eq;
use super::*;
@@ -1560,6 +1563,37 @@ mod tests {
"h"
);
}
#[test]
fn test_edit_indexer_prompt_prompt_confirmation_confirm() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1);
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::EditIndexerPrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert!(app.data.radarr_data.edit_indexer_modal.is_some());
assert!(app.should_refresh);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditIndexer(None))
);
}
}
#[test]
@@ -241,19 +241,35 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
}
fn handle_char_key_event(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.indexer_settings
.as_mut()
.unwrap()
.whitelisted_hardcoded_subs
)
match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.indexer_settings
.as_mut()
.unwrap()
.whitelisted_hardcoded_subs
)
}
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::IndexerSettingsConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::EditAllIndexerSettings(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -851,7 +851,13 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::assert_str_eq;
use crate::models::radarr_models::IndexerSettings;
use crate::{
models::{
radarr_models::IndexerSettings,
servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
use super::*;
@@ -909,6 +915,37 @@ mod tests {
"h"
);
}
#[test]
fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditAllIndexerSettings(None))
);
assert!(app.data.radarr_data.indexer_settings.is_some());
assert!(app.should_refresh);
}
}
#[test]
@@ -464,7 +464,10 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use crate::models::servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS;
use crate::{
models::servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
network::radarr_network::RadarrEvent,
};
use super::*;
@@ -696,6 +699,33 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
}
#[test]
fn test_delete_indexer_prompt_confirm() {
let mut app = App::default();
app
.data
.radarr_data
.indexers
.set_items(vec![Indexer::default()]);
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::DeleteIndexerPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteIndexer(None))
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
}
}
#[rstest]
+11 -2
View File
@@ -166,8 +166,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::Indexers {
match self.key {
match self.active_radarr_block {
ActiveRadarrBlock::Indexers => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.add.key => {
self
.app
@@ -194,7 +194,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
}
_ => (),
},
ActiveRadarrBlock::DeleteIndexerPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(None));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -461,6 +461,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
.tags
)
}
ActiveRadarrBlock::AddMoviePrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::AddMovieConfirmPrompt
&& *key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -1494,7 +1494,13 @@ mod tests {
mod test_handle_key_char {
use super::*;
use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::{
models::{
servarr_data::radarr::{modals::AddMovieModal, radarr_data::ADD_MOVIE_SELECTION_BLOCKS},
BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
#[test]
fn test_add_movie_search_input_backspace() {
@@ -1588,6 +1594,35 @@ mod tests {
"h"
);
}
#[test]
fn test_add_movie_confirm_prompt_prompt_confirmation_confirm() {
let mut app = App::default();
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into());
app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1);
AddMovieHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::AddMoviePrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::AddMovie(None))
);
assert!(app.data.radarr_data.add_movie_modal.is_some());
}
}
#[test]
@@ -1,3 +1,4 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -100,5 +101,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
}
}
fn handle_char_key_event(&mut self) {}
fn handle_char_key_event(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::DeleteMoviePrompt
&& self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::DeleteMovieConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
}
@@ -250,6 +250,51 @@ mod tests {
}
}
mod test_handle_key_char {
use crate::{
models::{
servarr_data::radarr::radarr_data::DELETE_MOVIE_SELECTION_BLOCKS, BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
use super::*;
#[test]
fn test_delete_movie_confirm_prompt_prompt_confirm() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into());
app.data.radarr_data.delete_movie_files = true;
app.data.radarr_data.add_list_exclusion = true;
app.data.radarr_data.selected_block =
BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(DELETE_MOVIE_SELECTION_BLOCKS.len() - 1);
DeleteMovieHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::DeleteMoviePrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteMovie(None))
);
assert!(app.should_refresh);
assert!(app.data.radarr_data.prompt_confirm);
assert!(app.data.radarr_data.delete_movie_files);
assert!(app.data.radarr_data.add_list_exclusion);
}
}
#[test]
fn test_delete_movie_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
@@ -327,6 +327,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
.tags
)
}
ActiveRadarrBlock::EditMoviePrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditMovieConfirmPrompt
&& *key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -940,7 +940,16 @@ mod tests {
mod test_handle_key_char {
use super::*;
use crate::models::servarr_data::radarr::modals::EditMovieModal;
use crate::{
models::{
servarr_data::radarr::{
modals::EditMovieModal,
radarr_data::{EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS},
},
BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
#[test]
fn test_edit_movie_path_input_backspace() {
@@ -1051,6 +1060,36 @@ mod tests {
"h"
);
}
#[test]
fn test_edit_movie_confirm_prompt_prompt_confirm() {
let mut app = App::default();
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default());
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into());
app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
EditMovieHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::EditMoviePrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditMovie(None))
);
assert!(app.data.radarr_data.edit_movie_modal.is_some());
assert!(app.should_refresh);
}
}
#[test]
@@ -996,6 +996,7 @@ mod tests {
RadarrData, EDIT_MOVIE_SELECTION_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
use crate::test_edit_movie_key;
use super::*;
@@ -1452,6 +1453,33 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert!(app.data.radarr_data.movies.sort.is_none());
}
#[test]
fn test_update_all_movies_prompt_confirm() {
let mut app = App::default();
app
.data
.radarr_data
.movies
.set_items(vec![Movie::default()]);
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into());
LibraryHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::UpdateAllMoviesPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::UpdateAllMovies)
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
}
}
#[rstest]
@@ -386,6 +386,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
self.app.data.radarr_data.movies.filter.as_mut().unwrap()
)
}
ActiveRadarrBlock::UpdateAllMoviesPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -47,18 +47,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) {
@@ -464,6 +474,32 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
}
_ => (),
},
ActiveRadarrBlock::AutomaticallySearchMoviePrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::TriggerAutomaticSearch(None));
self.app.pop_navigation_stack();
}
}
ActiveRadarrBlock::UpdateAndScanPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None));
self.app.pop_navigation_stack();
}
}
ActiveRadarrBlock::ManualSearchConfirmPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DownloadRelease(None));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -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;
@@ -1245,10 +1246,12 @@ 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![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into());
MovieDetailsHandler::with(
@@ -1468,6 +1471,7 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_MOVIE_SELECTION_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
use crate::test_edit_movie_key;
use super::*;
@@ -1485,10 +1489,17 @@ 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![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.search.key,
@@ -1538,10 +1549,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,
@@ -1669,10 +1679,17 @@ 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![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.update.key,
@@ -1732,10 +1749,17 @@ 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![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.refresh.key,
@@ -1780,6 +1804,50 @@ mod tests {
assert_eq!(app.get_current_route(), &active_radarr_block.into());
assert!(app.is_routing);
}
#[rstest]
#[case(
ActiveRadarrBlock::AutomaticallySearchMoviePrompt,
RadarrEvent::TriggerAutomaticSearch(None)
)]
#[case(
ActiveRadarrBlock::UpdateAndScanPrompt,
RadarrEvent::UpdateAndScan(None)
)]
#[case(
ActiveRadarrBlock::ManualSearchConfirmPrompt,
RadarrEvent::DownloadRelease(None)
)]
fn test_movie_info_prompt_confirm(
#[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent,
) {
let mut app = App::default();
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
movie_details: ScrollableText::with_string("test".to_owned()),
..MovieDetailsModal::default()
});
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
app.push_navigation_stack(prompt_block.into());
MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&prompt_block,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::MovieDetails.into()
);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
}
}
#[test]
@@ -1949,15 +2017,37 @@ 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![Release::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,
);
@@ -180,6 +180,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
self.app.data.radarr_data.edit_root_folder.as_mut().unwrap()
)
}
ActiveRadarrBlock::DeleteRootFolderPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteRootFolder(None));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -554,6 +554,8 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::network::radarr_network::RadarrEvent;
use super::*;
#[test]
@@ -706,6 +708,36 @@ mod tests {
"h"
);
}
#[test]
fn test_delete_root_folder_prompt_confirm() {
let mut app = App::default();
app
.data
.radarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into());
RootFoldersHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::DeleteRootFolderPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteRootFolder(None))
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::RootFolders.into()
);
}
}
#[test]
@@ -168,5 +168,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
{
self.app.should_refresh = true;
}
if self.active_radarr_block == &ActiveRadarrBlock::SystemTaskStartConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(None));
self.app.pop_navigation_stack();
}
}
}
@@ -858,6 +858,8 @@ mod tests {
mod test_handle_key_char {
use rstest::rstest;
use crate::network::radarr_network::RadarrEvent;
use super::*;
#[rstest]
@@ -912,6 +914,32 @@ mod tests {
assert_eq!(app.get_current_route(), &active_radarr_block.into());
assert!(!app.should_refresh);
}
#[test]
fn test_system_tasks_start_task_prompt_confirm() {
let mut app = App::default();
app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned());
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into());
SystemDetailsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::SystemTaskStartConfirmPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::StartTask(None))
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::SystemTasks.into()
);
}
}
#[test]
+94 -23
View File
@@ -1,12 +1,17 @@
#![warn(rust_2018_idioms)]
use std::fs::{self, File};
use std::io::BufReader;
use std::panic::PanicHookInfo;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
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,
};
@@ -14,12 +19,14 @@ use clap_complete::generate;
use colored::Colorize;
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, size, EnterAlternateScreen, LeaveAlternateScreen,
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 tokio::select;
use tokio::sync::mpsc::Receiver;
use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken;
@@ -41,9 +48,6 @@ mod network;
mod ui;
mod utils;
static MIN_TERM_WIDTH: u16 = 205;
static MIN_TERM_HEIGHT: u16 = 40;
#[derive(Debug, Parser)]
#[command(
name = crate_name!(),
@@ -62,6 +66,14 @@ static MIN_TERM_HEIGHT: u16 = 40;
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(
long,
global = true,
value_parser,
env = "MANAGARR_CONFIG_FILE",
help = "The Managarr configuration file to use"
)]
config: Option<PathBuf>,
}
#[tokio::main]
@@ -73,7 +85,13 @@ async fn main() -> Result<()> {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let args = Cli::parse();
let config = confy::load("managarr", "config")?;
let config = if let Some(ref config_file) = args.config {
load_config(config_file.to_str().expect("Invalid config file specified"))?
} else {
confy::load("managarr", "config")?
};
config.validate();
let reqwest_client = build_network_client(&config);
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
let cancellation_token = CancellationToken::new();
let ctrlc_cancellation_token = cancellation_token.clone();
@@ -95,7 +113,7 @@ async fn main() -> Result<()> {
Some(command) => match command {
Command::Radarr(_) => {
let app_nw = Arc::clone(&app);
let mut network = Network::new(&app_nw, cancellation_token);
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());
@@ -109,7 +127,9 @@ async fn main() -> Result<()> {
},
None => {
let app_nw = Arc::clone(&app);
std::thread::spawn(move || start_networking(sync_network_rx, &app_nw, cancellation_token));
std::thread::spawn(move || {
start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client)
});
start_ui(&app).await?;
}
}
@@ -122,28 +142,29 @@ async fn start_networking(
mut network_rx: Receiver<NetworkEvent>,
app: &Arc<Mutex<App<'_>>>,
cancellation_token: CancellationToken,
client: Client,
) {
let mut network = Network::new(app, cancellation_token);
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;
}
}
}
}
async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> {
let (width, height) = size()?;
if width < MIN_TERM_WIDTH || height < MIN_TERM_HEIGHT {
return Err(anyhow!(
"Terminal too small. Minimum size required: {}x{}; current terminal size: {}x{}",
MIN_TERM_WIDTH,
MIN_TERM_HEIGHT,
width,
height
));
}
let mut stdout = io::stdout();
enable_raw_mode()?;
@@ -212,6 +233,56 @@ 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()
.pool_max_idle_per_host(10)
.http2_keep_alive_interval(Duration::from_secs(5))
.tcp_keepalive(Duration::from_secs(5));
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};
+89
View File
@@ -57,6 +57,44 @@ 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>,
@@ -85,6 +123,26 @@ 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 {
@@ -223,6 +281,22 @@ 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 {
@@ -560,6 +634,17 @@ pub struct RootFolder {
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 {
@@ -647,6 +732,7 @@ pub enum RadarrSerdeable {
Credits(Vec<Credit>),
DiskSpaces(Vec<DiskSpace>),
DownloadsResponse(DownloadsResponse),
HostConfig(HostConfig),
Indexers(Vec<Indexer>),
IndexerSettings(IndexerSettings),
LogResponse(LogResponse),
@@ -657,6 +743,7 @@ pub enum RadarrSerdeable {
QueueEvents(Vec<QueueEvent>),
Releases(Vec<Release>),
RootFolders(Vec<RootFolder>),
SecurityConfig(SecurityConfig),
SystemStatus(SystemStatus),
Tags(Vec<Tag>),
Tasks(Vec<Task>),
@@ -686,6 +773,7 @@ serde_enum_from!(
Credits(Vec<Credit>),
DiskSpaces(Vec<DiskSpace>),
DownloadsResponse(DownloadsResponse),
HostConfig(HostConfig),
Indexers(Vec<Indexer>),
IndexerSettings(IndexerSettings),
LogResponse(LogResponse),
@@ -696,6 +784,7 @@ serde_enum_from!(
QueueEvents(Vec<QueueEvent>),
Releases(Vec<Release>),
RootFolders(Vec<RootFolder>),
SecurityConfig(SecurityConfig),
SystemStatus(SystemStatus),
Tags(Vec<Tag>),
Tasks(Vec<Task>),
+31 -4
View File
@@ -5,14 +5,41 @@ mod tests {
use crate::models::{
radarr_models::{
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, DiskSpace,
DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult, Log,
LogResponse, MinimumAvailability, Monitor, Movie, MovieHistoryItem, QualityProfile,
QueueEvent, RadarrSerdeable, Release, RootFolder, SystemStatus, Tag, Task, TaskName, Update,
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,
},
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!(
+11 -6
View File
@@ -40,7 +40,7 @@ pub trait NetworkTrait {
#[derive(Clone)]
pub struct Network<'a, 'b> {
client: Client,
cancellation_token: CancellationToken,
pub cancellation_token: CancellationToken,
pub app: &'a Arc<Mutex<App<'b>>>,
}
@@ -62,14 +62,22 @@ impl<'a, 'b> NetworkTrait for Network<'a, 'b> {
}
impl<'a, 'b> Network<'a, 'b> {
pub fn new(app: &'a Arc<Mutex<App<'b>>>, cancellation_token: CancellationToken) -> Self {
pub fn new(
app: &'a Arc<Mutex<App<'b>>>,
cancellation_token: CancellationToken,
client: Client,
) -> Self {
Network {
client: Client::new(),
client,
app,
cancellation_token,
}
}
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>,
@@ -85,9 +93,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() => {
+35 -12
View File
@@ -6,6 +6,7 @@ mod tests {
use mockito::{Mock, Server, ServerGuard};
use pretty_assertions::assert_str_eq;
use reqwest::Client;
use rstest::rstest;
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, Mutex};
@@ -25,7 +26,7 @@ mod tests {
.with_body("{}")
.create_async()
.await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned();
let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse()
@@ -37,10 +38,12 @@ mod tests {
host,
api_token: String::new(),
port,
ssl_cert_path: None,
..RadarrConfig::default()
};
app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let _ = network
.handle_network_event(RadarrEvent::HealthCheck.into())
@@ -64,7 +67,7 @@ mod tests {
.create_async()
.await;
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let _ = network
.handle_request::<Test, ()>(
@@ -90,7 +93,7 @@ mod tests {
#[values(RequestMethod::Get, RequestMethod::Post)] request_method: RequestMethod,
) {
let (async_server, app_arc, server) = mock_api(request_method, 200, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -122,7 +125,7 @@ mod tests {
#[values(RequestMethod::Get, RequestMethod::Post)] request_method: RequestMethod,
) {
let (async_server, app_arc, server) = mock_api(request_method, 400, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let mut test_result = String::new();
let resp = network
@@ -160,7 +163,7 @@ mod tests {
cancellation_token.clone(),
)));
app_arc.lock().await.is_loading = true;
let mut network = Network::new(&app_arc, cancellation_token);
let mut network = Network::new(&app_arc, cancellation_token, Client::new());
network.cancellation_token.cancel();
let resp = network
@@ -178,11 +181,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;
@@ -194,7 +217,7 @@ mod tests {
.create_async()
.await;
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -226,7 +249,7 @@ mod tests {
#[tokio::test]
async fn test_handle_request_failure_to_send_request() {
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -266,7 +289,7 @@ mod tests {
request_method: RequestMethod,
) {
let (async_server, app_arc, server) = mock_api(request_method, 404, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -296,7 +319,7 @@ mod tests {
#[tokio::test]
async fn test_handle_request_non_success_code_empty_response_body() {
let (async_server, app_arc, server) = mock_api(RequestMethod::Post, 404, false).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -354,7 +377,7 @@ mod tests {
async_server = async_server.create_async().await;
let app_arc = Arc::new(Mutex::new(App::default()));
let network = Network::new(&app_arc, CancellationToken::new());
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
network
.call_api(RequestProps {
+59 -7
View File
@@ -11,10 +11,10 @@ use crate::app::RadarrConfig;
use crate::models::radarr_models::{
AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, BlocklistResponse, Collection,
CollectionMovie, CommandBody, Credit, CreditType, DeleteMovieParams, DiskSpace, DownloadRecord,
DownloadsResponse, EditCollectionParams, EditIndexerParams, EditMovieParams, Indexer,
DownloadsResponse, EditCollectionParams, EditIndexerParams, EditMovieParams, HostConfig, Indexer,
IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody, MovieHistoryItem,
QualityProfile, QueueEvent, RadarrSerdeable, Release, ReleaseDownloadBody, RootFolder,
SystemStatus, Tag, Task, TaskName, Update,
SecurityConfig, SystemStatus, Tag, Task, TaskName, Update,
};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
@@ -50,6 +50,7 @@ pub enum RadarrEvent {
GetBlocklist,
GetCollections,
GetDownloads,
GetHostConfig,
GetIndexers,
GetAllIndexerSettings,
GetLogs(Option<u64>),
@@ -62,6 +63,7 @@ pub enum RadarrEvent {
GetQueuedEvents,
GetReleases(Option<i64>),
GetRootFolders,
GetSecurityConfig,
GetStatus,
GetTags,
GetTasks,
@@ -86,6 +88,7 @@ impl RadarrEvent {
RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
RadarrEvent::GetCollections | RadarrEvent::EditCollection(_) => "/collection",
RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload(_) => "/queue",
RadarrEvent::GetHostConfig | RadarrEvent::GetSecurityConfig => "/config/host",
RadarrEvent::GetIndexers | RadarrEvent::EditIndexer(_) | RadarrEvent::DeleteIndexer(_) => {
"/indexer"
}
@@ -179,14 +182,15 @@ impl<'a, 'b> Network<'a, 'b> {
self.edit_indexer(params).await.map(RadarrSerdeable::from)
}
RadarrEvent::EditMovie(params) => self.edit_movie(params).await.map(RadarrSerdeable::from),
RadarrEvent::GetBlocklist => self.get_blocklist().await.map(RadarrSerdeable::from),
RadarrEvent::GetCollections => self.get_collections().await.map(RadarrSerdeable::from),
RadarrEvent::GetDownloads => self.get_downloads().await.map(RadarrSerdeable::from),
RadarrEvent::GetIndexers => self.get_indexers().await.map(RadarrSerdeable::from),
RadarrEvent::GetAllIndexerSettings => self
.get_all_indexer_settings()
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetBlocklist => self.get_blocklist().await.map(RadarrSerdeable::from),
RadarrEvent::GetCollections => self.get_collections().await.map(RadarrSerdeable::from),
RadarrEvent::GetDownloads => self.get_downloads().await.map(RadarrSerdeable::from),
RadarrEvent::GetHostConfig => self.get_host_config().await.map(RadarrSerdeable::from),
RadarrEvent::GetIndexers => self.get_indexers().await.map(RadarrSerdeable::from),
RadarrEvent::GetLogs(events) => self.get_logs(events).await.map(RadarrSerdeable::from),
RadarrEvent::GetMovieCredits(movie_id) => {
self.get_credits(movie_id).await.map(RadarrSerdeable::from)
@@ -209,6 +213,7 @@ impl<'a, 'b> Network<'a, 'b> {
self.get_releases(movie_id).await.map(RadarrSerdeable::from)
}
RadarrEvent::GetRootFolders => self.get_root_folders().await.map(RadarrSerdeable::from),
RadarrEvent::GetSecurityConfig => self.get_security_config().await.map(RadarrSerdeable::from),
RadarrEvent::GetStatus => self.get_status().await.map(RadarrSerdeable::from),
RadarrEvent::GetTags => self.get_tags().await.map(RadarrSerdeable::from),
RadarrEvent::GetTasks => self.get_tasks().await.map(RadarrSerdeable::from),
@@ -1354,6 +1359,22 @@ impl<'a, 'b> Network<'a, 'b> {
.await
}
async fn get_host_config(&mut self) -> Result<HostConfig> {
info!("Fetching Radarr host config");
let request_props = self
.radarr_request_props_from(
RadarrEvent::GetHostConfig.resource(),
RequestMethod::Get,
None::<()>,
)
.await;
self
.handle_request::<(), HostConfig>(request_props, |_, _| ())
.await
}
async fn get_indexers(&mut self) -> Result<Vec<Indexer>> {
info!("Fetching Radarr indexers");
@@ -1765,6 +1786,22 @@ impl<'a, 'b> Network<'a, 'b> {
.await
}
async fn get_security_config(&mut self) -> Result<SecurityConfig> {
info!("Fetching Radarr security config");
let request_props = self
.radarr_request_props_from(
RadarrEvent::GetSecurityConfig.resource(),
RequestMethod::Get,
None::<()>,
)
.await;
self
.handle_request::<(), SecurityConfig>(request_props, |_, _| ())
.await
}
async fn get_status(&mut self) -> Result<SystemStatus> {
info!("Fetching Radarr system status");
@@ -2224,9 +2261,24 @@ impl<'a, 'b> Network<'a, 'b> {
let RadarrConfig {
host,
port,
uri,
api_token,
ssl_cert_path,
} = &app.config.radarr;
let uri = format!("http://{host}:{}/api/v3{resource}", port.unwrap_or(7878));
let uri = if let Some(radarr_uri) = uri {
format!("{radarr_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(7878)
)
};
RequestProps {
uri,
+201 -100
View File
@@ -6,6 +6,7 @@ mod test {
use chrono::{DateTime, Utc};
use mockito::{Matcher, Mock, Server, ServerGuard};
use pretty_assertions::{assert_eq, assert_str_eq};
use reqwest::Client;
use rstest::rstest;
use serde_json::{json, Number, Value};
use strum::IntoEnumIterator;
@@ -174,6 +175,13 @@ mod test {
assert_str_eq!(event.resource(), "/queue");
}
#[rstest]
fn test_resource_host_config(
#[values(RadarrEvent::GetHostConfig, RadarrEvent::GetSecurityConfig)] event: RadarrEvent,
) {
assert_str_eq!(event.resource(), "/config/host");
}
#[rstest]
fn test_resource_command(
#[values(
@@ -229,7 +237,7 @@ mod test {
RadarrEvent::HealthCheck.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let _ = network.handle_radarr_event(RadarrEvent::HealthCheck).await;
@@ -255,7 +263,7 @@ mod test {
RadarrEvent::GetOverview.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let disk_space_vec = vec![
DiskSpace {
free_space: 1111,
@@ -294,7 +302,7 @@ mod test {
RadarrEvent::GetStatus.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap())
as DateTime<Utc>;
@@ -378,7 +386,7 @@ mod test {
.movies
.sorting(vec![title_sort_option]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Movies(movies) = network
.handle_radarr_event(RadarrEvent::GetMovies)
@@ -433,7 +441,7 @@ mod test {
.radarr_data
.movies
.sorting(vec![title_sort_option]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetMovies)
@@ -486,7 +494,7 @@ mod test {
.movies
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Releases(releases_vec) = network
.handle_radarr_event(RadarrEvent::GetReleases(None))
@@ -544,7 +552,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetReleases(None))
@@ -603,7 +611,7 @@ mod test {
)
.await;
app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::AddMovieSearchResults(add_movie_search_results) = network
.handle_radarr_event(RadarrEvent::SearchNewMovie(None))
@@ -669,7 +677,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::AddMovieSearchResults(add_movie_search_results) = network
.handle_radarr_event(RadarrEvent::SearchNewMovie(Some("test term".into())))
@@ -704,7 +712,7 @@ mod test {
task_name: TaskName::default(),
..Task::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::StartTask(None))
@@ -729,7 +737,7 @@ mod test {
RadarrEvent::StartTask(None).resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::StartTask(Some(TaskName::default())))
@@ -750,7 +758,7 @@ mod test {
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Get, None, Some(json!([])), None, &resource).await;
app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::SearchNewMovie(None))
@@ -786,7 +794,7 @@ mod test {
.match_header("X-Api-Key", "test1234");
async_server = async_server.expect_at_most(0).create_async().await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned();
let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse()
@@ -797,10 +805,11 @@ mod test {
host,
port,
api_token: "test1234".to_owned(),
..RadarrConfig::default()
};
app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::SearchNewMovie(None))
@@ -879,7 +888,7 @@ mod test {
.radarr_data
.indexers
.set_items(vec![indexer()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::TestIndexer(None))
@@ -947,7 +956,7 @@ mod test {
.radarr_data
.indexers
.set_items(vec![indexer()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::TestIndexer(None))
@@ -1008,7 +1017,7 @@ mod test {
.with_body("{}")
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::TestIndexer(Some(1)))
@@ -1085,7 +1094,7 @@ mod test {
.radarr_data
.indexers
.set_items(indexers);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::IndexerTestResults(results) = network
.handle_radarr_event(RadarrEvent::TestAllIndexers)
@@ -1136,7 +1145,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(None))
@@ -1159,7 +1168,7 @@ mod test {
RadarrEvent::TriggerAutomaticSearch(None).resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(Some(1)))
@@ -1189,7 +1198,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateAndScan(None))
@@ -1212,7 +1221,7 @@ mod test {
RadarrEvent::UpdateAndScan(None).resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateAndScan(Some(1)))
@@ -1235,7 +1244,7 @@ mod test {
RadarrEvent::UpdateAllMovies.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateAllMovies)
@@ -1257,7 +1266,7 @@ mod test {
RadarrEvent::UpdateDownloads.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateDownloads)
@@ -1279,7 +1288,7 @@ mod test {
RadarrEvent::UpdateCollections.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateCollections)
@@ -1310,7 +1319,7 @@ mod test {
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.quality_profile_map =
BiMap::from_iter([(2222, "HD - 1080p".to_owned())]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Movie(movie) = network
.handle_radarr_event(RadarrEvent::GetMovieDetails(None))
@@ -1395,7 +1404,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Movie(movie) = network
.handle_radarr_event(RadarrEvent::GetMovieDetails(Some(1)))
@@ -1449,7 +1458,7 @@ mod test {
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.quality_profile_map =
BiMap::from_iter([(2222, "HD - 1080p".to_owned())]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetMovieDetails(None))
@@ -1523,7 +1532,7 @@ mod test {
.movies
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::MovieHistoryItems(history) = network
.handle_radarr_event(RadarrEvent::GetMovieHistory(None))
@@ -1571,7 +1580,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::MovieHistoryItems(history) = network
.handle_radarr_event(RadarrEvent::GetMovieHistory(Some(1)))
@@ -1611,7 +1620,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetMovieHistory(None))
@@ -1760,7 +1769,7 @@ mod test {
.blocklist
.sorting(vec![blocklist_sort_option]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::BlocklistResponse(blocklist) = network
.handle_radarr_event(RadarrEvent::GetBlocklist)
@@ -1881,7 +1890,7 @@ mod test {
.radarr_data
.blocklist
.sorting(vec![blocklist_sort_option]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetBlocklist)
@@ -2004,7 +2013,7 @@ mod test {
.collections
.sorting(vec![collection_sort_option]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Collections(collections) = network
.handle_radarr_event(RadarrEvent::GetCollections)
@@ -2111,7 +2120,7 @@ mod test {
.radarr_data
.collections
.sorting(vec![collection_sort_option]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetCollections)
@@ -2155,7 +2164,7 @@ mod test {
RadarrEvent::GetDownloads.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::DownloadsResponse(downloads) = network
.handle_radarr_event(RadarrEvent::GetDownloads)
@@ -2171,6 +2180,40 @@ mod test {
}
}
#[tokio::test]
async fn test_handle_get_host_config_event() {
let host_config_response = json!({
"bindAddress": "*",
"port": 7878,
"urlBase": "some.test.site/radarr",
"instanceName": "Radarr",
"applicationUrl": "https://some.test.site:7878/radarr",
"enableSsl": true,
"sslPort": 9898,
"sslCertPath": "/app/radarr.pfx",
"sslCertPassword": "test"
});
let response: HostConfig = serde_json::from_value(host_config_response.clone()).unwrap();
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(host_config_response),
None,
RadarrEvent::GetHostConfig.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::HostConfig(host_config) = network
.handle_radarr_event(RadarrEvent::GetHostConfig)
.await
.unwrap()
{
async_server.assert_async().await;
assert_eq!(host_config, response);
}
}
#[tokio::test]
async fn test_handle_get_indexers_event() {
let indexers_response_json = json!([{
@@ -2212,7 +2255,7 @@ mod test {
RadarrEvent::GetIndexers.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Indexers(indexers) = network
.handle_radarr_event(RadarrEvent::GetIndexers)
@@ -2251,7 +2294,7 @@ mod test {
RadarrEvent::GetAllIndexerSettings.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::IndexerSettings(settings) = network
.handle_radarr_event(RadarrEvent::GetAllIndexerSettings)
@@ -2289,7 +2332,7 @@ mod test {
)
.await;
app_arc.lock().await.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetAllIndexerSettings)
@@ -2336,7 +2379,7 @@ mod test {
RadarrEvent::GetQueuedEvents.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::QueueEvents(events) = network
.handle_radarr_event(RadarrEvent::GetQueuedEvents)
@@ -2397,7 +2440,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::LogResponse(logs) = network
.handle_radarr_event(RadarrEvent::GetLogs(None))
@@ -2467,7 +2510,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::LogResponse(logs) = network
.handle_radarr_event(RadarrEvent::GetLogs(Some(1000)))
@@ -2508,7 +2551,7 @@ mod test {
RadarrEvent::GetQualityProfiles.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::QualityProfiles(quality_profiles) = network
.handle_radarr_event(RadarrEvent::GetQualityProfiles)
@@ -2539,7 +2582,7 @@ mod test {
RadarrEvent::GetTags.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Tags(tags) = network
.handle_radarr_event(RadarrEvent::GetTags)
@@ -2601,7 +2644,7 @@ mod test {
RadarrEvent::GetTasks.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Tasks(tasks) = network
.handle_radarr_event(RadarrEvent::GetTasks)
@@ -2694,7 +2737,7 @@ mod test {
RadarrEvent::GetUpdates.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Updates(updates) = network
.handle_radarr_event(RadarrEvent::GetUpdates)
@@ -2711,7 +2754,7 @@ mod test {
}
#[tokio::test]
async fn test_add_tag() {
async fn test_handle_add_tag() {
let tag_json = json!({ "id": 3, "label": "testing" });
let response: Tag = serde_json::from_value(tag_json.clone()).unwrap();
let (async_server, app_arc, _server) = mock_radarr_api(
@@ -2724,7 +2767,7 @@ mod test {
.await;
app_arc.lock().await.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Tag(tag) = network
.handle_radarr_event(RadarrEvent::AddTag("testing".to_owned()))
@@ -2749,7 +2792,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteTag(1).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteTag(1))
@@ -2776,7 +2819,7 @@ mod test {
RadarrEvent::GetRootFolders.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::RootFolders(root_folders) = network
.handle_radarr_event(RadarrEvent::GetRootFolders)
@@ -2792,6 +2835,38 @@ mod test {
}
}
#[tokio::test]
async fn test_handle_get_security_config_event() {
let security_config_response = json!({
"authenticationMethod": "forms",
"authenticationRequired": "disabledForLocalAddresses",
"username": "test",
"password": "some password",
"apiKey": "someApiKey12345",
"certificateValidation": "disabledForLocalAddresses",
});
let response: SecurityConfig =
serde_json::from_value(security_config_response.clone()).unwrap();
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(security_config_response),
None,
RadarrEvent::GetSecurityConfig.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::SecurityConfig(security_config) = network
.handle_radarr_event(RadarrEvent::GetSecurityConfig)
.await
.unwrap()
{
async_server.assert_async().await;
assert_eq!(security_config, response);
}
}
#[tokio::test]
async fn test_handle_get_movie_credits_event() {
let credits_json = json!([
@@ -2828,7 +2903,7 @@ mod test {
.movies
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Credits(credits) = network
.handle_radarr_event(RadarrEvent::GetMovieCredits(None))
@@ -2873,7 +2948,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Credits(credits) = network
.handle_radarr_event(RadarrEvent::GetMovieCredits(Some(1)))
@@ -2919,7 +2994,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetMovieCredits(None))
@@ -2948,7 +3023,7 @@ mod test {
app.data.radarr_data.delete_movie_files = true;
app.data.radarr_data.add_list_exclusion = true;
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteMovie(None))
@@ -2968,7 +3043,7 @@ mod test {
);
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let delete_movie_params = DeleteMovieParams {
id: 1,
delete_movie_files: true,
@@ -3017,7 +3092,7 @@ mod test {
.radarr_data
.blocklist
.set_items(blocklist_items);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::ClearBlocklist)
@@ -3039,7 +3114,7 @@ mod test {
.radarr_data
.blocklist
.set_items(vec![blocklist_item()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteBlocklistItem(None))
@@ -3054,7 +3129,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem(None).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteBlocklistItem(Some(1)))
@@ -3076,7 +3151,7 @@ mod test {
.radarr_data
.downloads
.set_items(vec![download_record()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteDownload(None))
@@ -3091,7 +3166,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteDownload(None).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteDownload(Some(1)))
@@ -3113,7 +3188,7 @@ mod test {
.radarr_data
.indexers
.set_items(vec![indexer()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteIndexer(None))
@@ -3128,7 +3203,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteIndexer(None).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteIndexer(Some(1)))
@@ -3150,7 +3225,7 @@ mod test {
.radarr_data
.root_folders
.set_items(vec![root_folder()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteRootFolder(None))
@@ -3165,7 +3240,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteRootFolder(None).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteRootFolder(Some(1)))
@@ -3249,7 +3324,7 @@ mod test {
app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
}
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddMovie(None))
@@ -3302,7 +3377,7 @@ mod test {
},
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddMovie(Some(body)))
@@ -3388,7 +3463,7 @@ mod test {
add_searched_movies.scroll_to_bottom();
app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddMovie(None))
@@ -3432,7 +3507,7 @@ mod test {
.await;
app_arc.lock().await.data.radarr_data.edit_root_folder = Some("/nfs/test".into());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddRootFolder(None))
@@ -3462,7 +3537,7 @@ mod test {
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddRootFolder(Some("/test/test".to_owned())))
@@ -3502,7 +3577,7 @@ mod test {
.await;
app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditAllIndexerSettings(None))
@@ -3541,7 +3616,7 @@ mod test {
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditAllIndexerSettings(
@@ -3639,7 +3714,7 @@ mod test {
app.data.radarr_data.quality_profile_map =
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditCollection(None))
@@ -3724,7 +3799,7 @@ mod test {
root_folder_path: Some("/nfs/Test Path".to_owned()),
search_on_add: Some(false),
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditCollection(Some(edit_collection_params)))
@@ -3803,7 +3878,7 @@ mod test {
collection_id: 123,
..EditCollectionParams::default()
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditCollection(Some(edit_collection_params)))
@@ -3899,7 +3974,7 @@ mod test {
app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal);
app.data.radarr_data.indexers.set_items(vec![indexer()]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(None))
@@ -4000,7 +4075,7 @@ mod test {
);
app.data.radarr_data.indexers.set_items(vec![indexer]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(None))
@@ -4115,7 +4190,7 @@ mod test {
);
app.data.radarr_data.indexers.set_items(vec![indexer]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(None))
@@ -4210,7 +4285,7 @@ mod test {
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)))
@@ -4270,7 +4345,7 @@ mod test {
.match_body(Matcher::Json(indexer_details_json))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)))
@@ -4355,7 +4430,7 @@ mod test {
.match_body(Matcher::Json(expected_edit_indexer_body))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)))
@@ -4418,7 +4493,7 @@ mod test {
app.data.radarr_data.quality_profile_map =
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditMovie(None))
@@ -4469,7 +4544,7 @@ mod test {
tags: Some(vec![1, 2]),
..EditMovieParams::default()
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params)))
@@ -4506,7 +4581,7 @@ mod test {
movie_id: 1,
..EditMovieParams::default()
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params)))
@@ -4547,7 +4622,7 @@ mod test {
clear_tags: true,
..EditMovieParams::default()
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params)))
@@ -4584,7 +4659,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DownloadRelease(None))
@@ -4608,7 +4683,7 @@ mod test {
RadarrEvent::DownloadRelease(None).resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let params = ReleaseDownloadBody {
guid: "1234".to_owned(),
indexer_id: 2,
@@ -4635,7 +4710,7 @@ mod test {
(3, "hi".to_owned()),
]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(
network.extract_and_add_tag_ids_vec(tags).await,
@@ -4663,7 +4738,7 @@ mod test {
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let tag_ids_vec = network.extract_and_add_tag_ids_vec(tags).await;
@@ -4692,7 +4767,7 @@ mod test {
id: 1,
..Movie::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(network.extract_movie_id().await, 1);
}
@@ -4706,7 +4781,7 @@ mod test {
..Movie::default()
}]);
app_arc.lock().await.data.radarr_data.movies = filtered_movies;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(network.extract_movie_id().await, 1);
}
@@ -4724,7 +4799,7 @@ mod test {
id: 1,
..Collection::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(network.extract_collection_id().await, 1);
}
@@ -4738,7 +4813,7 @@ mod test {
..Collection::default()
}]);
app_arc.lock().await.data.radarr_data.collections = filtered_collections;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(network.extract_collection_id().await, 1);
}
@@ -4756,7 +4831,7 @@ mod test {
id: 1,
..Movie::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_str_eq!(
network.append_movie_id_param("/test", None).await,
@@ -4777,7 +4852,7 @@ mod test {
id: 1,
..Movie::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_str_eq!(
network.append_movie_id_param("/test", Some(11)).await,
@@ -4788,7 +4863,7 @@ mod test {
#[tokio::test]
async fn test_radarr_request_props_from_default_radarr_config() {
let app_arc = Arc::new(Mutex::new(App::default()));
let network = Network::new(&app_arc, CancellationToken::new());
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let request_props = network
.radarr_request_props_from("/test", RequestMethod::Get, None::<()>)
@@ -4800,9 +4875,10 @@ mod test {
assert!(request_props.api_token.is_empty());
app_arc.lock().await.config.radarr = RadarrConfig {
host: "192.168.0.123".to_owned(),
host: Some("192.168.0.123".to_owned()),
port: Some(8080),
api_token: "testToken1234".to_owned(),
..RadarrConfig::default()
};
}
@@ -4811,17 +4887,41 @@ mod test {
let api_token = "testToken1234".to_owned();
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc.lock().await.config.radarr = RadarrConfig {
host: "192.168.0.123".to_owned(),
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()),
..RadarrConfig::default()
};
let network = Network::new(&app_arc, CancellationToken::new());
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let request_props = network
.radarr_request_props_from("/test", RequestMethod::Get, None::<()>)
.await;
assert_str_eq!(request_props.uri, "http://192.168.0.123:8080/api/v3/test");
assert_str_eq!(request_props.uri, "https://192.168.0.123:8080/api/v3/test");
assert_eq!(request_props.method, RequestMethod::Get);
assert_eq!(request_props.body, None);
assert_str_eq!(request_props.api_token, api_token);
}
#[tokio::test]
async fn test_radarr_request_props_from_custom_radarr_config_using_uri_instead_of_host_and_port()
{
let api_token = "testToken1234".to_owned();
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc.lock().await.config.radarr = RadarrConfig {
uri: Some("https://192.168.0.123:8080".to_owned()),
api_token: api_token.clone(),
..RadarrConfig::default()
};
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let request_props = network
.radarr_request_props_from("/test", RequestMethod::Get, None::<()>)
.await;
assert_str_eq!(request_props.uri, "https://192.168.0.123:8080/api/v3/test");
assert_eq!(request_props.method, RequestMethod::Get);
assert_eq!(request_props.body, None);
assert_str_eq!(request_props.api_token, api_token);
@@ -4906,7 +5006,7 @@ mod test {
async_server = async_server.create_async().await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned();
let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse()
@@ -4917,6 +5017,7 @@ mod test {
host,
port,
api_token: "test1234".to_owned(),
..RadarrConfig::default()
};
app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app));
+4 -4
View File
@@ -1,6 +1,6 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text};
use ratatui::widgets::Clear;
@@ -83,7 +83,7 @@ fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.select(app.server_tabs.index);
let help = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Right);
.right_aligned();
f.render_widget(tabs, tabs_area);
f.render_widget(help, help_area);
@@ -170,7 +170,7 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -
.select(tab_state.index);
let help = Paragraph::new(Text::from(tab_state.get_active_tab_help().help()))
.block(borderless_block())
.alignment(Alignment::Right);
.right_aligned();
f.render_widget(tabs, tabs_area);
f.render_widget(help, help_area);
@@ -197,7 +197,7 @@ pub fn draw_input_box_popup(
let help = Paragraph::new("<esc> cancel")
.help()
.alignment(Alignment::Center)
.centered()
.block(borderless_block());
f.render_widget(help, help_area);
}
@@ -259,7 +259,7 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.wrap(Wrap { trim: false });
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
.centered();
f.render_widget(paragraph, paragraph_area);
f.render_widget(help_paragraph, help_area);
@@ -1,9 +1,12 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::ListItem;
use ratatui::text::Text;
use ratatui::widgets::{ListItem, Paragraph};
use ratatui::Frame;
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{
@@ -110,7 +113,7 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
let selected_minimum_availability = minimum_availability_list.current_selection();
let selected_quality_profile = quality_profile_list.current_selection();
let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, root_folder_area, search_on_add_area, _, buttons_area] =
let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, root_folder_area, search_on_add_area, _, buttons_area, help_area] =
Layout::vertical([
Constraint::Length(6),
Constraint::Length(3),
@@ -118,8 +121,9 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
@@ -127,6 +131,8 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area);
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
let prompt_paragraph = layout_paragraph_borderless(&collection_overview);
let monitored_checkbox = Checkbox::new("Monitored")
.highlighted(selected_block == &ActiveRadarrBlock::EditCollectionToggleMonitored)
@@ -169,6 +175,7 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
f.render_widget(search_on_add_checkbox, search_on_add_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
}
fn draw_edit_collection_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
+5 -1
View File
@@ -92,7 +92,11 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
);
}
let percent = 1f64 - (*sizeleft as f64 / *size as f64);
let percent = if *size == 0 {
0.0
} else {
1f64 - (*sizeleft as f64 / *size as f64)
};
let file_size: f64 = convert_to_gb(*size);
Row::new(vec![
+17 -8
View File
@@ -1,5 +1,7 @@
use std::sync::atomic::Ordering;
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::Route;
@@ -14,6 +16,8 @@ use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::popup::Size;
use crate::ui::{draw_popup_over, DrawUi};
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::Paragraph;
use ratatui::Frame;
#[cfg(test)]
@@ -50,32 +54,36 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let highlight_yes_no = selected_block == &ActiveRadarrBlock::EditIndexerConfirmPrompt;
let edit_indexer_modal_option = &app.data.radarr_data.edit_indexer_modal;
let protocol = &app.data.radarr_data.indexers.current_selection().protocol;
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
if edit_indexer_modal_option.is_some() {
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
let [settings_area, buttons_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(3)])
.margin(1)
.areas(area);
let [settings_area, _, buttons_area, help_area] = Layout::vertical([
Constraint::Length(15),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let [left_side_area, right_side_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.margin(1)
.areas(settings_area);
let [name_area, rss_area, auto_search_area, interactive_search_area, _] = Layout::vertical([
let [name_area, rss_area, auto_search_area, interactive_search_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
])
.areas(left_side_area);
let [url_area, api_key_area, seed_ratio_area, tags_area, _] = Layout::vertical([
let [url_area, api_key_area, seed_ratio_area, tags_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
])
.areas(right_side_area);
@@ -161,6 +169,7 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(interactive_search_checkbox, interactive_search_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
}
} else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
@@ -1,8 +1,12 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
@@ -52,33 +56,37 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
let selected_block = app.data.radarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == &ActiveRadarrBlock::IndexerSettingsConfirmPrompt;
let indexer_settings_option = &app.data.radarr_data.indexer_settings;
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
if indexer_settings_option.is_some() {
let indexer_settings = indexer_settings_option.as_ref().unwrap();
let [settings_area, buttons_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(3)])
.margin(1)
.areas(area);
let [settings_area, _, buttons_area, help_area] = Layout::vertical([
Constraint::Length(15),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let [left_side_area, right_side_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.margin(1)
.areas(settings_area);
let [min_age_area, retention_area, max_size_area, prefer_flags_area, _] = Layout::vertical([
let [min_age_area, retention_area, max_size_area, prefer_flags_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
])
.areas(left_side_area);
let [availability_delay_area, rss_sync_interval_area, whitelisted_sub_tags_area, allow_hardcoded_subs_area, _] =
let [availability_delay_area, rss_sync_interval_area, whitelisted_sub_tags_area, allow_hardcoded_subs_area] =
Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
])
.areas(right_side_area);
@@ -162,6 +170,7 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
f.render_widget(allow_hardcoded_subs_checkbox, allow_hardcoded_subs_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
} else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
+13 -7
View File
@@ -1,12 +1,14 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
use ratatui::Frame;
use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES};
use crate::app::radarr::radarr_context_clues::ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES;
use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, CONFIRMATION_PROMPT_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};
@@ -209,7 +211,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
.centered();
search_box.show_cursor(f, search_box_area);
f.render_widget(layout_block(), results_area);
@@ -220,7 +222,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
.centered();
let error_message = Message::new("No movies found matching your query!");
let error_message_popup = Popup::new(error_message).size(Size::Message);
@@ -240,7 +242,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Text::from(build_context_clue_string(&ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
.centered();
let search_results_table = ManagarrTable::new(
app.data.radarr_data.add_searched_movies.as_mut(),
search_results_row_mapping,
@@ -369,7 +371,7 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(title_block_centered(&title), area);
let [paragraph_area, root_folder_area, monitor_area, min_availability_area, quality_profile_area, tags_area, _, buttons_area] =
let [paragraph_area, root_folder_area, monitor_area, min_availability_area, quality_profile_area, tags_area, _, buttons_area, help_area] =
Layout::vertical([
Constraint::Length(6),
Constraint::Length(3),
@@ -377,14 +379,18 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let prompt_paragraph = layout_paragraph_borderless(&prompt);
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
f.render_widget(prompt_paragraph, paragraph_area);
f.render_widget(help_paragraph, help_area);
let [add_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
+10 -3
View File
@@ -2,9 +2,12 @@ use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::Layout;
use ratatui::widgets::ListItem;
use ratatui::text::Text;
use ratatui::widgets::{ListItem, Paragraph};
use ratatui::Frame;
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{
@@ -113,7 +116,7 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
let selected_minimum_availability = minimum_availability_list.current_selection();
let selected_quality_profile = quality_profile_list.current_selection();
let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, path_area, tags_area, _, buttons_area] =
let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, path_area, tags_area, _, buttons_area, help_area] =
Layout::vertical([
Constraint::Length(6),
Constraint::Length(3),
@@ -121,8 +124,9 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
@@ -130,6 +134,8 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area);
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
let prompt_paragraph = layout_paragraph_borderless(&movie_overview);
let monitored_checkbox = Checkbox::new("Monitored")
.checked(monitored.unwrap_or_default())
@@ -181,6 +187,7 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
f.render_widget(quality_profile_drop_down_button, quality_profile_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
}
fn draw_edit_movie_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
+79 -71
View File
@@ -1,6 +1,6 @@
use std::iter;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
@@ -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) {
@@ -510,7 +518,7 @@ fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) {
let content_paragraph = Paragraph::new(lines_vec)
.block(borderless_block())
.wrap(Wrap { trim: false })
.alignment(Alignment::Left);
.left_aligned();
let confirmation_prompt = ConfirmationPrompt::new()
.title(title)
.prompt(&prompt)
+7 -3
View File
@@ -1,7 +1,7 @@
use std::iter;
use chrono::{Duration, Utc};
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::Stylize;
use ratatui::text::Text;
use ratatui::widgets::{Paragraph, Row};
@@ -192,7 +192,11 @@ fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
size,
..
} = &downloads_vec[i];
let percent = 1f64 - (*sizeleft as f64 / *size as f64);
let percent = if *size == 0 {
0.0
} else {
1f64 - (*sizeleft as f64 / *size as f64)
};
let download_gauge = line_gauge_with_title(title, percent);
f.render_widget(download_gauge, download_item_areas[i]);
@@ -244,6 +248,6 @@ fn draw_radarr_logo(f: &mut Frame<'_>, area: Rect) {
let logo = Paragraph::new(logo_text)
.light_yellow()
.block(layout_block().default())
.alignment(Alignment::Center);
.centered();
f.render_widget(logo, area);
}
+2 -2
View File
@@ -1,7 +1,7 @@
use std::ops::Sub;
use chrono::Utc;
use ratatui::layout::{Alignment, Layout};
use ratatui::layout::Layout;
use ratatui::style::Style;
use ratatui::text::{Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row};
@@ -204,7 +204,7 @@ fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
);
let help_paragraph = Paragraph::new(help_text)
.block(layout_block_top_border())
.alignment(Alignment::Left);
.left_aligned();
f.render_widget(help_paragraph, area);
}
+1 -1
View File
@@ -43,7 +43,7 @@ pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> {
.primary()
.bold()
.wrap(Wrap { trim: false })
.alignment(Alignment::Center)
.centered()
}
pub fn borderless_block<'a>() -> Block<'a> {
+5 -5
View File
@@ -1,7 +1,7 @@
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{layout_block, style_block_highlight};
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::prelude::{Style, Text, Widget};
use ratatui::style::Styled;
use ratatui::widgets::Paragraph;
@@ -58,11 +58,11 @@ impl<'a> Button<'a> {
if let Some(icon) = self.icon {
layout_block().style(style).render(area, buf);
Paragraph::new(Text::from(self.title))
.alignment(Alignment::Left)
.left_aligned()
.style(style)
.render(title_area, buf);
Paragraph::new(Text::from(format!("{icon} ")))
.alignment(Alignment::Right)
.right_aligned()
.style(style)
.render(icon_area, buf);
}
@@ -72,7 +72,7 @@ impl<'a> Button<'a> {
let [label_area, button_area] =
Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area);
let label_paragraph = Paragraph::new(Text::from(format!("\n{}: ", self.label.unwrap())))
.alignment(Alignment::Right)
.right_aligned()
.primary();
if self.icon.is_some() {
@@ -87,7 +87,7 @@ impl<'a> Button<'a> {
fn render_button(self, area: Rect, buf: &mut Buffer) {
Paragraph::new(Text::from(self.title))
.block(layout_block())
.alignment(Alignment::Center)
.centered()
.style(style_block_highlight(self.is_selected))
.render(area, buf);
}
+3 -3
View File
@@ -1,7 +1,7 @@
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{borderless_block, layout_block, style_block_highlight};
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::Text;
use ratatui::style::Stylize;
use ratatui::widgets::{Paragraph, Widget};
@@ -47,13 +47,13 @@ impl<'a> Checkbox<'a> {
Paragraph::new(Text::from(format!("\n{}: ", self.label)))
.block(borderless_block())
.alignment(Alignment::Right)
.right_aligned()
.primary()
.render(label_area, buf);
Paragraph::new(Text::from(check))
.block(layout_block())
.alignment(Alignment::Center)
.centered()
.style(style_block_highlight(self.is_highlighted).bold())
.render(checkbox_box_area, buf);
}
+27 -8
View File
@@ -1,9 +1,12 @@
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{layout_paragraph_borderless, title_block_centered};
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::{Paragraph, Widget};
use std::iter;
@@ -64,12 +67,16 @@ impl<'a> ConfirmationPrompt<'a> {
fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) {
title_block_centered(self.title).render(area, buf);
let help_text =
Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
if let Some(checkboxes) = self.checkboxes {
let mut constraints = vec![
Constraint::Length(4),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
];
constraints.splice(
1..1,
@@ -81,6 +88,7 @@ impl<'a> ConfirmationPrompt<'a> {
.areas(chunks[checkboxes.len() + 2]);
layout_paragraph_borderless(self.prompt).render(chunks[0], buf);
help_paragraph.render(chunks[checkboxes.len() + 3], buf);
checkboxes
.into_iter()
@@ -102,26 +110,37 @@ impl<'a> ConfirmationPrompt<'a> {
fn render_confirmation_prompt(self, area: Rect, buf: &mut Buffer) {
title_block_centered(self.title).render(area, buf);
let help_text =
Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
let [prompt_area, buttons_area] = if let Some(content_paragraph) = self.content {
let [prompt_area, content_area, _, buttons_area] = Layout::vertical([
let [prompt_area, content_area, _, buttons_area, help_area] = Layout::vertical([
Constraint::Length(4),
Constraint::Length(7),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
content_paragraph.render(content_area, buf);
help_paragraph.render(help_area, buf);
[prompt_area, buttons_area]
} else {
let [prompt_area, buttons_area] =
Layout::vertical([Constraint::Percentage(72), Constraint::Length(3)])
.margin(1)
.flex(Flex::SpaceBetween)
.areas(area);
let [prompt_area, buttons_area, _, help_area] = Layout::vertical([
Constraint::Percentage(72),
Constraint::Length(3),
Constraint::Fill(0),
Constraint::Min(1),
])
.margin(1)
.flex(Flex::SpaceBetween)
.areas(area);
help_paragraph.render(help_area, buf);
[prompt_area, buttons_area]
};
+2 -2
View File
@@ -1,5 +1,5 @@
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Layout, Position, Rect};
use ratatui::layout::{Constraint, Layout, Position, Rect};
use ratatui::prelude::Text;
use ratatui::style::{Style, Styled, Stylize};
use ratatui::widgets::{Block, Paragraph, Widget};
@@ -114,7 +114,7 @@ impl<'a> InputBox<'a> {
Paragraph::new(Text::from(format!("\n{label}: ")))
.block(borderless_block())
.alignment(Alignment::Right)
.right_aligned()
.primary()
.render(label_area, buf);
input_box_paragraph.render(text_box_area, buf);
+3 -3
View File
@@ -1,7 +1,7 @@
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{background_block, centered_rect, layout_block_top_border};
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::Text;
use ratatui::widgets::{Block, Clear, Paragraph, Widget};
@@ -27,7 +27,7 @@ impl Size {
pub fn to_percent(&self) -> (u16, u16) {
match self {
Size::SmallPrompt => (20, 20),
Size::Prompt => (35, 35),
Size::Prompt => (37, 37),
Size::LargePrompt => (70, 45),
Size::Message => (25, 8),
Size::NarrowMessage => (50, 20),
@@ -100,7 +100,7 @@ impl<'a, T: Widget> Popup<'a, T> {
Paragraph::new(Text::from(format!(" {footer}").help()))
.block(layout_block_top_border())
.alignment(Alignment::Left)
.left_aligned()
.render(help_footer_area, buf);
content_area
+1 -1
View File
@@ -7,7 +7,7 @@ mod tests {
#[test]
fn test_dimensions_to_percent() {
assert_eq!(Size::SmallPrompt.to_percent(), (20, 20));
assert_eq!(Size::Prompt.to_percent(), (35, 35));
assert_eq!(Size::Prompt.to_percent(), (37, 37));
assert_eq!(Size::LargePrompt.to_percent(), (70, 45));
assert_eq!(Size::Message.to_percent(), (25, 8));
assert_eq!(Size::NarrowMessage.to_percent(), (50, 20));