Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6df68b8a66
|
|||
|
cbca6bd916
|
|||
|
92187c5f16
|
|||
|
366809d8c6
|
|||
|
dd93fe117d
|
|||
|
10e18af1bf
|
|||
|
a4d93692a9
|
|||
|
03d134bec9
|
|||
|
4cad9e1755
|
|||
| adb76bb603 | |||
| d9a2b1c6c4 | |||
|
47a2fbf0cc
|
|||
|
7067902752
|
|||
|
ba1cf0182b
|
|||
|
5cccec88c9
|
|||
|
bbcd3f00a9
|
|||
| 2e339dd73b | |||
|
f988cf0f26
|
|||
|
ff82dc2012
|
|||
| 89a692ad90 | |||
| d77ec5fb34 | |||
| ec90e2dca7 | |||
|
5a4e6c9623
|
|||
|
9a6a06ee20
|
|||
|
5556e48fc0
|
|||
|
af573cac2a
|
|||
|
447cf6a2b4
|
|||
|
203bf9cb66
|
|||
|
4f9bc34d23
|
|||
|
a2aa9507a9
|
|||
|
c791b985f0
|
|||
|
5c517a748c
|
|||
|
892c687077
|
|||
|
c6d5b98e86
|
|||
|
67e5114ec2
|
|||
|
fdc331865e
|
|||
|
f388dccc08
|
|||
|
64fad3b9bc
|
|||
|
3be7b09da8
|
|||
|
5f3123cd79
|
|||
|
d8f7febfe1
|
|||
|
0bfbb44e3e
|
|||
| c5161f828d | |||
| 71c64167f0 | |||
| 4d3e00fd94 | |||
| 9e96e74c87 | |||
| ddb869c341 | |||
| f17f542e8e | |||
| a2e6400a38 | |||
| 89f5ff6bc7 | |||
| eff1a901eb | |||
| 7add62b245 | |||
| 5fa9b08347 |
@@ -0,0 +1,11 @@
|
||||
### AI assistance (if any):
|
||||
- List tools here and files touched by them
|
||||
|
||||
### Authorship & Understanding
|
||||
|
||||
- [ ] I wrote or heavily modified this code myself
|
||||
- [ ] I understand how it works end-to-end
|
||||
- [ ] I can maintain this code in the future
|
||||
- [ ] No undisclosed AI-generated code was used
|
||||
- [ ] If AI assistance was used, it is documented below
|
||||
|
||||
@@ -76,15 +76,15 @@ jobs:
|
||||
RUSTDOCFLAGS: --cfg docsrs
|
||||
msrv:
|
||||
# check that we can build using the minimal rust version that is specified by this crate
|
||||
name: 1.89.0 / check
|
||||
name: 1.95.0 / check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install 1.89.0
|
||||
- name: Install 1.95.0
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.89.0
|
||||
toolchain: 1.95.0
|
||||
|
||||
- name: cargo +1.89.0 check
|
||||
- name: cargo +1.95.0 check
|
||||
run: cargo check
|
||||
|
||||
+106
@@ -5,6 +5,112 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.7.2 (2026-04-20)
|
||||
|
||||
### Feat
|
||||
|
||||
- Created a separate 'ssl' property for the config so users don't have to specify an ssl_cert_path to use SSL or use the uri workaround for HTTPS API access
|
||||
|
||||
## v0.7.1 (2026-02-04)
|
||||
|
||||
### Feat
|
||||
|
||||
- Added support for a system-wide notification popup mechanism that works across Servarrs
|
||||
- Implemented a 'config-path' command to print out the default Managarr configuration file path to help address #54
|
||||
- Full support for filtering disks and aggregating root folders in the UI's 'Stats' block
|
||||
- proper collapsing of root folder paths in the stats layer of the UI
|
||||
- Added config option to filter for specific disk space paths to display in the UI (CLI is unaffected)
|
||||
- Improved disk-space UI and CLI that shows the actual path being monitored instead of just a disk number
|
||||
- Implemented the forgotten lidarr list disk-space command
|
||||
|
||||
### Fix
|
||||
|
||||
- Improved the system notification feature so it can persist between modals
|
||||
- Sonarr API updated to somtimes allow either seeders or leechers to be null
|
||||
- Improved the first-time run behavior so that it outputs the default configuration file it tries to load to help users locate the file on first-runs
|
||||
- 'managarr config-path' should work without a pre-existing config already in place [#54]
|
||||
|
||||
### Refactor
|
||||
|
||||
- Removed the filtering of monitored_storage_paths from the networking module and migrated all of it to the UI
|
||||
|
||||
## v0.7.0 (2026-01-21)
|
||||
|
||||
### Feat
|
||||
|
||||
- Blocklist support in Lidarr in both the CLI and TUI
|
||||
- CLI and TUI support for track history and track details in Lidarr
|
||||
- Lidarr UI support for album details popup
|
||||
- Implemented TUI handler support for the Album Details popup in Lidarr
|
||||
- Bulk added CLI support for tracks and album functionalities in Lidarr
|
||||
- Implemented the manual artist discography search tab in Lidarr's artist details UI
|
||||
- Lidarr CLI support for downloading a release
|
||||
- CLI support for searching for discography releases in Lidarr
|
||||
- Added TUI and CLI support for viewing Artist history in Lidarr
|
||||
- Full Lidarr system support for both the CLI and TUI
|
||||
- Full CLI and TUI support for the Lidarr Indexers tab
|
||||
- Full support for adding a root folder in Lidarr from both the CLI and TUI
|
||||
- naive lidarr root folder tab implementation. Needs improved add logic
|
||||
- Downloads tab support in Lidarr
|
||||
- Created a History tab in the Radarr UI and created a list history command and mark-history-item-as-failed command for Radarr
|
||||
- Implemented the Lidarr History tab and CLI support
|
||||
- TUI support for deleting a Lidarr album from the artist details popup
|
||||
- CLI support for deleting an album from Lidarr
|
||||
- Completed support for viewing Lidarr artist details
|
||||
- Full CLI and TUI support for adding an artist to Lidarr
|
||||
- Include the Lidarr artist disambiguation in the title of the Edit Artist popup
|
||||
- Initial Lidarr support for searching for new artists
|
||||
- Lidarr CLI commands to list quality profiles and metadata profiles
|
||||
- Improved CLI readability by creating a separate Global Options section for global flags
|
||||
- CLI support for deleting a tag in Lidarr
|
||||
- Lidarr CLI support for listing and adding tags
|
||||
- Added CLI and TUI support for editing Lidarr artists
|
||||
- Support for updating all Lidarr artists in both the CLI and TUI
|
||||
- Added Lidarr CLI support for fetching the host config and the security config
|
||||
- Created Lidarr commands: 'get artist-details' and 'get system-status'
|
||||
- Fetch the artist members as part of the artist details query
|
||||
- Support for toggling the monitoring of a given artist via the CLI and TUI
|
||||
- Full support for deleting an artist via CLI and TUI
|
||||
- TUI support for Lidarr library
|
||||
- CLI support for listing artists
|
||||
- Improved UI speed and responsiveness
|
||||
|
||||
### Fix
|
||||
|
||||
- Sonarr network wasn't checking for the user to be using the sorting block when populating season details
|
||||
- Sonarr CLI was not properly filtering out episode and season releases when manually searching for releases
|
||||
- Sonarr manual search TUI and CLI incorrectly displaying the same unfiltered results for both season and episode searches
|
||||
- Slowed down the automatic text scrolling in tables so the text is readable
|
||||
- Expanded the history item details size so that it can include all the available information for a given item; was previously being cut off on some screens
|
||||
- Bug in submitting the update series prompt in the series details UI in Sonarr
|
||||
- Don't include Lidarr artist disambiguation in Edit popup title when it is empty
|
||||
- Refactored how quality profiles, language profiles, and metadata profiles are populated for each servarr so they sort using the ID to mimic the web UI better
|
||||
- Added the correct keybinding context to the Lidarr edit artist popup
|
||||
- Improved fault tolerance for search result tables and test all indexer results tables
|
||||
- Prevented additional empty slice errors in indexer tables
|
||||
- Fixed a bug in all Servarr implementations to not try to get the current selection of a search table when an error is returned from the API
|
||||
- Fixed an issue with the Managarr table that would incorrectly try to display things before is_loading was ready
|
||||
- Fixed a bug where the edit collection popup would not display when opening it from collection details
|
||||
|
||||
### Refactor
|
||||
|
||||
- Refactored the SonarrEvent enum to not unnecessarily wrap dual series_id and season_number values in a tuple when both values can be passed directly
|
||||
- Improved and simplified the implementation of history details for both Sonarr and Lidarr
|
||||
- Let serde serialize Add Series and Add Movie enums instead of calling to_string up front
|
||||
- Use is_multiple_of for the tick counter in the UI module
|
||||
- Updated all model tests to use purpose-built assertions to improve readability and maintainability
|
||||
- Updated all handler tests to use purpose built assertions to improve readability and maintainability
|
||||
- Used is_multiple_of to make life easier and cleaner in the app module
|
||||
- Refactored all cli tests to use purpose-built assertions
|
||||
- Improved test assertions in the app module
|
||||
- Created dedicated proptests and assertions to clean up the handler unit tests
|
||||
- Migrated the handle_table_events macro into a trait for better IDE support, created a TableEventAdapter wrapper for the KeyEventHandlers to make it so that the trait can be used properly and a simple function to replace the previous call to the handle_table_events macro
|
||||
- Simplified both the table_handler macro and the stateful_table implementation
|
||||
- Improved error handling for the tail-logs subcommand to propagate errors up the stack instead of exiting there.
|
||||
- Added accessor methods to servarr_data structs, replaced for loops with functional iterator chains, eliminated mutable state tracking, and updated network module to use get_or_insert_default() for modal options
|
||||
- Improved error handling project-wide and cleaned up some regexes with unnecessary escapes (tail_logs and interpolate_env_vars)
|
||||
- Refactored to use more idiomatic let-else statements where applicable
|
||||
|
||||
## v0.6.3 (2025-12-13)
|
||||
|
||||
### Fix
|
||||
|
||||
@@ -91,5 +91,12 @@ Then, you can run workflows locally without having to commit and see if the GitH
|
||||
act -W .github/workflows/release.yml --input_type bump=minor
|
||||
```
|
||||
|
||||
## Authorship Policy
|
||||
|
||||
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
|
||||
Claude, etc.) is not permitted unless explicitly disclosed and approved.
|
||||
|
||||
Submissions must certify that the contributor understands and can maintain the code they submit.
|
||||
|
||||
## Questions? Reach out to me!
|
||||
If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can!
|
||||
|
||||
Generated
+1002
-733
File diff suppressed because it is too large
Load Diff
+36
-36
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "managarr"
|
||||
version = "0.6.3"
|
||||
version = "0.7.2"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "A TUI and CLI to manage your Servarrs"
|
||||
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
|
||||
@@ -10,7 +10,7 @@ homepage = "https://github.com/Dark-Alex-17/managarr"
|
||||
readme = "README.md"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
rust-version = "1.89.0"
|
||||
rust-version = "1.95.0"
|
||||
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
|
||||
[workspace]
|
||||
@@ -20,67 +20,67 @@ members = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.68"
|
||||
backtrace = "0.3.74"
|
||||
anyhow = "1.0.100"
|
||||
backtrace = "0.3.76"
|
||||
bimap = { version = "0.6.3", features = ["serde"] }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
confy = { version = "0.6.0", default-features = false, features = [
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
confy = { version = "2.0.0", default-features = false, features = [
|
||||
"yaml_conf",
|
||||
] }
|
||||
crossterm = "0.28.1"
|
||||
derivative = "2.2.0"
|
||||
human-panic = "2.0.2"
|
||||
indoc = "2.0.0"
|
||||
log = "0.4.17"
|
||||
log4rs = { version = "1.2.0", features = ["file_appender"] }
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.9", features = ["json"] }
|
||||
serde_yaml = "0.9.16"
|
||||
serde_json = "1.0.91"
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
human-panic = "2.0.6"
|
||||
indoc = "2.0.7"
|
||||
log = "0.4.29"
|
||||
log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] }
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.13.2", features = ["json"] }
|
||||
serde_yaml = "0.9.34"
|
||||
serde_json = "1.0.149"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
strum_macros = "0.26.4"
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
tokio-util = "0.7.8"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
tokio-util = "0.7.18"
|
||||
ratatui = { version = "0.30.0", features = [
|
||||
"all-widgets",
|
||||
"unstable-widget-ref",
|
||||
] }
|
||||
urlencoding = "2.1.2"
|
||||
clap = { version = "4.5.20", features = [
|
||||
urlencoding = "2.1.3"
|
||||
clap = { version = "4.5.56", features = [
|
||||
"derive",
|
||||
"cargo",
|
||||
"env",
|
||||
"wrap_help",
|
||||
] }
|
||||
clap_complete = "4.5.33"
|
||||
clap_complete = "4.5.65"
|
||||
itertools = "0.14.0"
|
||||
ctrlc = "3.4.5"
|
||||
colored = "3.0.0"
|
||||
async-trait = "0.1.83"
|
||||
ctrlc = "3.5.1"
|
||||
colored = "3.1.1"
|
||||
async-trait = "0.1.89"
|
||||
dirs-next = "2.0.0"
|
||||
managarr-tree-widget = "0.25.0"
|
||||
indicatif = "0.17.9"
|
||||
derive_setters = "0.1.6"
|
||||
deunicode = "1.6.0"
|
||||
openssl = { version = "0.10.70", features = ["vendored"] }
|
||||
indicatif = "0.17.11"
|
||||
derive_setters = "0.1.9"
|
||||
deunicode = "1.6.2"
|
||||
openssl = { version = "0.10.79", features = ["vendored"] }
|
||||
veil = "0.2.0"
|
||||
validate_theme_derive = "0.1.0"
|
||||
enum_display_style_derive = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.16"
|
||||
mockall = "0.13.0"
|
||||
mockito = "1.0.0"
|
||||
pretty_assertions = "1.3.0"
|
||||
proptest = "1.6.0"
|
||||
assert_cmd = "2.1.2"
|
||||
mockall = "0.13.1"
|
||||
mockito = "1.7.1"
|
||||
pretty_assertions = "1.4.1"
|
||||
proptest = "1.9.0"
|
||||
rstest = "0.25.0"
|
||||
serial_test = "3.2.0"
|
||||
assertables = "9.8.2"
|
||||
insta = "1.41.1"
|
||||
serial_test = "3.3.1"
|
||||
assertables = "9.8.4"
|
||||
insta = "1.46.1"
|
||||
|
||||
[dev-dependencies.cargo-husky]
|
||||
version = "1"
|
||||
version = "1.5.0"
|
||||
default-features = false
|
||||
features = ["user-hooks"]
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ RUN mv target/release/managarr .
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the compiled binary from the builder container
|
||||
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||

|
||||

|
||||

|
||||

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

|
||||
[](https://codecov.io/gh/Dark-Alex-17/managarr)
|
||||
@@ -12,17 +11,16 @@
|
||||

|
||||
[](https://matrix.to/#/#managarr:matrix.org)
|
||||
|
||||
|
||||
Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust!
|
||||
|
||||

|
||||

|
||||
|
||||
## What Servarrs are supported?
|
||||
|
||||
- [x]  [Radarr](https://wiki.servarr.com/radarr)
|
||||
- [x]  [Sonarr](https://wiki.servarr.com/en/sonarr)
|
||||
- [x]  [Lidarr](https://wiki.servarr.com/en/lidarr)
|
||||
- [ ]  [Readarr](https://wiki.servarr.com/en/readarr)
|
||||
- [ ]  [Lidarr](https://wiki.servarr.com/en/lidarr)
|
||||
- [ ]  [Prowlarr](https://wiki.servarr.com/en/prowlarr)
|
||||
- [ ]  [Whisparr](https://wiki.servarr.com/whisparr)
|
||||
- [ ]  [Bazarr](https://www.bazarr.media/)
|
||||
@@ -56,9 +54,9 @@ Run Managarr as a docker container by mounting your `config.yml` file to `/root/
|
||||
docker run --rm -it -v /home/aclarke/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr:latest
|
||||
```
|
||||
|
||||
You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command.
|
||||
You can also clone this repo and run `just build-docker` to build a docker image locally and run it using the above command.
|
||||
|
||||
Please note that you will need to create and popular your configuration file first before starting the container. Otherwise, the container will fail to start.
|
||||
Please note that you will need to create and populate your configuration file first before starting the container. Otherwise, the container will fail to start.
|
||||
|
||||
**Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path.
|
||||
|
||||
@@ -96,7 +94,7 @@ of Chocolatey packages take quite some time, and thus the package may not be ava
|
||||
choco install managarr
|
||||
|
||||
# Some newer releases may require a version number, so you can specify it like so:
|
||||
choco install managarr --version=0.5.0
|
||||
choco install managarr --version=0.7.0
|
||||
```
|
||||
|
||||
To upgrade to the latest and greatest version of Managarr:
|
||||
@@ -104,7 +102,7 @@ To upgrade to the latest and greatest version of Managarr:
|
||||
choco upgrade managarr
|
||||
|
||||
# To upgrade to a specific version:
|
||||
choco upgrade managarr --version=0.5.0
|
||||
choco upgrade managarr --version=0.7.0
|
||||
```
|
||||
|
||||
### Manual
|
||||
@@ -182,14 +180,30 @@ Key:
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
|
||||
### Lidarr
|
||||
|
||||
| TUI | CLI | Feature |
|
||||
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||
| ✅ | ✅ | View your library, downloads, blocklist, tracks |
|
||||
| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library |
|
||||
| ✅ | ✅ | Add artists to your library |
|
||||
| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files |
|
||||
| ✅ | ✅ | Trigger automatic searches for artists or albums |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads |
|
||||
| ✅ | ✅ | Manually search for full artist discographies or albums |
|
||||
| ✅ | ✅ | Edit your artists and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
|
||||
### Readarr
|
||||
|
||||
- [ ] Support for Readarr
|
||||
|
||||
### Lidarr
|
||||
|
||||
- [ ] Support for Lidarr
|
||||
|
||||
### Whisparr
|
||||
|
||||
- [ ] Support for Whisparr
|
||||
@@ -231,7 +245,7 @@ To see all available commands, simply run `managarr --help`:
|
||||
|
||||
```shell
|
||||
$ managarr --help
|
||||
managarr 0.5.1
|
||||
managarr 0.7.0
|
||||
Alex Clarke <alex.j.tusa@gmail.com>
|
||||
|
||||
A TUI and CLI to manage your Servarrs
|
||||
@@ -241,20 +255,35 @@ Usage: managarr [OPTIONS] [COMMAND]
|
||||
Commands:
|
||||
radarr Commands for manging your Radarr instance
|
||||
sonarr Commands for manging your Sonarr instance
|
||||
lidarr Commands for manging your Lidarr instance
|
||||
completions Generate shell completions for the Managarr CLI
|
||||
tail-logs Tail Managarr logs
|
||||
config-path Print the full path to the default configuration file.
|
||||
This file can be changed to another location using the '--config-file' flag
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||
--config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||
--themes-file <THEMES_FILE> The Managarr themes file to use [env: MANAGARR_THEMES_FILE=]
|
||||
--theme <THEME> The name of the Managarr theme to use [env: MANAGARR_THEME=]
|
||||
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
||||
This is useful when you have multiple instances of the same Servarr defined in your config file.
|
||||
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
|
||||
Global Options:
|
||||
--disable-spinner Disable the spinner (can sometimes make parsing output
|
||||
challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||
--config-file <CONFIG_FILE> The Managarr configuration file to use; defaults to the
|
||||
path shown by 'managarr config-path' [env:
|
||||
MANAGARR_CONFIG_FILE=]
|
||||
--themes-file <THEMES_FILE> The Managarr themes file to use [env:
|
||||
MANAGARR_THEMES_FILE=]
|
||||
--theme <THEME> The name of the Managarr theme to use [env:
|
||||
MANAGARR_THEME=]
|
||||
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify
|
||||
the name of the instance configuration that you want to
|
||||
use.
|
||||
|
||||
This is useful when you have multiple instances of the
|
||||
same Servarr defined in your config file.
|
||||
By default, if left empty, the first configured Servarr
|
||||
instance listed in the config file will be used.
|
||||
```
|
||||
|
||||
All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run:
|
||||
@@ -283,12 +312,21 @@ Commands:
|
||||
test-all-indexers Test all Sonarr indexers
|
||||
toggle-episode-monitoring Toggle monitoring for the specified episode
|
||||
toggle-season-monitoring Toggle monitoring for the specified season that corresponds to the specified series ID
|
||||
toggle-series-monitoring Toggle monitoring for the specified series corresponding to the given series ID
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||
-h, --help Print help
|
||||
|
||||
Global Options:
|
||||
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||
--config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||
--themes-file <THEMES_FILE> The Managarr themes file to use [env: MANAGARR_THEMES_FILE=]
|
||||
--theme <THEME> The name of the Managarr theme to use [env: MANAGARR_THEME=]
|
||||
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
||||
|
||||
This is useful when you have multiple instances of the same Servarr defined in your config file.
|
||||
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
|
||||
```
|
||||
|
||||
**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:
|
||||
@@ -300,23 +338,44 @@ $ managarr radarr list movies | jq '.[] | select(.title == "Ad Astra") | .id'
|
||||
|
||||
# Configuration
|
||||
Managarr assumes reasonable defaults to connect to each service (i.e. Radarr is on localhost:7878),
|
||||
but all servers will require you to input the API token.
|
||||
but all servers will require you to input the API token. This means that for each Servarr you configure,
|
||||
if you define only the `api_token`, Managarr will assume the Servarr is running on `localhost` and on the
|
||||
default port for that respective service. That is:
|
||||
|
||||
The configuration file is located somewhere different for each OS.
|
||||
| Servarr | Default Host | Default Port |
|
||||
|---------|--------------|--------------|
|
||||
| Radarr | `localhost` | 7878 |
|
||||
| Sonarr | `localhost` | 8989 |
|
||||
| Lidarr | `localhost` | 8686 |
|
||||
|
||||
### Linux
|
||||
```
|
||||
$HOME/.config/managarr/config.yml
|
||||
> [!TIP]
|
||||
> In general, all Servarrs store their API tokens under Settings -> General -> Security -> API Key in their web UIs.
|
||||
|
||||
## Minimum Configuration Requirements
|
||||
The following configuration file will connect to each Servarr running on localhost with their default ports. The only
|
||||
requirement for each is the specification of an API token.
|
||||
|
||||
```yaml
|
||||
radarr:
|
||||
# Connect to Radarr running on localhost:7878
|
||||
- api_token: <your-radarr-api-token-here>
|
||||
|
||||
sonarr:
|
||||
# Connect to sonarr running on localhost:8989
|
||||
- api_token: <your-sonarr-api-token-here>
|
||||
|
||||
lidarr:
|
||||
# Connect to lidarr running on localhost:8686
|
||||
- api_token: <your-lidarr-api-token-here>
|
||||
```
|
||||
|
||||
### Mac
|
||||
```
|
||||
$HOME/Library/Application Support/managarr/config.yml
|
||||
```
|
||||
## Configuration File Location
|
||||
|
||||
### Windows
|
||||
```
|
||||
%APPDATA%/Roaming/managarr/config.yml
|
||||
The configuration file is located somewhere different for each OS, so run the following command to print out the default
|
||||
location of the `managarr` configuration file for your system:
|
||||
|
||||
```shell
|
||||
managarr config-path
|
||||
```
|
||||
|
||||
## Specify Which Configuration File to Use
|
||||
@@ -335,43 +394,102 @@ radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: someApiToken1234567890
|
||||
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
|
||||
sonarr:
|
||||
- uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
|
||||
ssl_cert_path: /path/to/radarr.crt # Use the specified SSL certificate to connect to this Servarr
|
||||
# Enables SSL regardless of the value of the 'ssl'
|
||||
# See the SSL Configuration section below for more information
|
||||
|
||||
- host: 192.168.0.79
|
||||
port: 7878
|
||||
api_token: someApiToken1234567890
|
||||
ssl: true # Use SSL to connect to this Servarr
|
||||
# This will assume that you have the SSL certificate installed to your system trust store
|
||||
# See the SSL Configuration section below for more information
|
||||
|
||||
- uri: http://htpc.local/radarr # Example of using the 'uri' key instead of 'host' and 'port'
|
||||
api_token: someApiToken1234567890
|
||||
|
||||
sonarr:
|
||||
- host: 192.168.0.89
|
||||
port: 8989
|
||||
api_token_file: /root/.config/sonarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file
|
||||
|
||||
- name: Anime Sonarr # An example of a custom name for a secondary Sonarr instance
|
||||
host: 192.168.0.89
|
||||
host: 192.168.1.89
|
||||
port: 8989
|
||||
api_token: someApiToken1234567890
|
||||
readarr:
|
||||
- host: 192.168.0.87
|
||||
port: 8787
|
||||
api_token_file: /root/.config/readarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file
|
||||
|
||||
lidarr:
|
||||
- host: 192.168.0.86
|
||||
port: 8686
|
||||
api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables
|
||||
whisparr:
|
||||
- host: 192.168.0.69
|
||||
port: 6969
|
||||
monitored_storage_paths: # Filter which Root Folders or Disk Storage you want displayed in the UI's 'Stats' block
|
||||
# Note: Setting these values does not affect what shows up in the 'Root Folders' tab of the UI.
|
||||
- /nfs # An example disk (i.e. '<servarr> list disk-space' command) you want displayed in the UI under 'Storage:'
|
||||
- /media # An example root folder you want displayed in the UI
|
||||
# Root folders collapse up to the super-directory to reduce duplication in the UI. For example:
|
||||
# if you have root folders '/media/tv', '/media/cartoons' and '/media/reality', and you set this
|
||||
# monitored path, the UI will show '/media/[tv,cartoons,reality]' under Root Folders
|
||||
|
||||
- host: 192.168.1.86
|
||||
port: 8686
|
||||
api_token: someApiToken1234567890
|
||||
ssl_cert_path: /path/to/whisparr.crt
|
||||
ssl_cert_path: /path/to/lidarr_1.crt
|
||||
custom_headers: # Example of adding custom headers to all requests to the Servarr instance
|
||||
traefik-auth-bypass-key: someBypassKey1234567890
|
||||
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
|
||||
bazarr:
|
||||
- host: 192.168.0.67
|
||||
port: 6767
|
||||
api_token: someApiToken1234567890
|
||||
prowlarr:
|
||||
- host: 192.168.0.96
|
||||
port: 9696
|
||||
api_token: someApiToken1234567890
|
||||
tautulli:
|
||||
- host: 192.168.0.81
|
||||
port: 8181
|
||||
api_token: someApiToken1234567890
|
||||
```
|
||||
|
||||
### SSL Configuration
|
||||
If your Servarr is using SSL or self-signed certificates, you may need to specify additional configuration options to connect without issues.
|
||||
|
||||
|
||||
**If your Servarr's domain CA is installed in the system's trust store:**
|
||||
Then you can simply specify `ssl: true` and Managarr will be able to connect to your Servarr:
|
||||
|
||||
```yaml
|
||||
radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: yourApiTokenHere
|
||||
ssl: true
|
||||
```
|
||||
|
||||
|
||||
**If your Servarr's domain CA is not installed:**
|
||||
You'll either need to specify the path to the certificate via the `ssl_cert_path` property, or you'll need to install the certificate into your system store.
|
||||
|
||||
To acquire the cert for your Servarr's domain, you can use the following command:
|
||||
```shell
|
||||
openssl s_client -show-certs -connect <your-servarr-domain.com>:<port> </dev/null |\
|
||||
sed -n -e '/-.BEGIN/,/-.END/ p' > /path/to/your/servarr.pem
|
||||
```
|
||||
|
||||
Now, you can either specify `ssl_cert_path: /path/to/your/servarr.pem`:
|
||||
|
||||
Example configuration with a certificate that's not installed to the system trust store:
|
||||
```yaml
|
||||
radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: yourApiTokenHere
|
||||
ssl_cert_path: /path/to/your/certificate.crt
|
||||
```
|
||||
|
||||
Or install the certificate into your system's trust store.
|
||||
|
||||
For example, if you're on a Debian-based system and have `ca-certificates` installed (`sudo apt install ca-certificates`):
|
||||
```shell
|
||||
sudo mv /path/to/your/servarr.pem /usr/local/share/ca-certificates/servarr.pem
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
Example configuration with a certificate that is installed to the system trust store:
|
||||
```yaml
|
||||
radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: yourApiTokenHere
|
||||
ssl: true
|
||||
```
|
||||
|
||||
### Example Multi-Instance Configuration:
|
||||
@@ -428,9 +546,6 @@ Managarr supports using environment variables on startup so you don't have to al
|
||||
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
|
||||
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
|
||||
|
||||
## Track What I'm Currently Working On
|
||||
To see what feature(s) I'm currently working on, check out my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr).
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Radarr
|
||||
@@ -446,6 +561,13 @@ To see what feature(s) I'm currently working on, check out my [Wekan Board](http
|
||||

|
||||

|
||||
|
||||
### Lidarr
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### General
|
||||

|
||||

|
||||
@@ -461,8 +583,8 @@ To see what feature(s) I'm currently working on, check out my [Wekan Board](http
|
||||
## Servarr Requirements
|
||||
* [Radarr >= 5.3.6.8612](https://radarr.video/docs/api/)
|
||||
* [Sonarr >= v4](https://sonarr.tv/docs/api/)
|
||||
* [Readarr v1](https://readarr.com/docs/api/)
|
||||
* [Lidarr v1](https://lidarr.audio/docs/api/)
|
||||
* [Readarr v1](https://readarr.com/docs/api/)
|
||||
* [Whisparr >= v3](https://whisparr.com/docs/api/)
|
||||
* [Prowlarr v1](https://prowlarr.com/docs/api/)
|
||||
* [Bazarr v1.1.4](http://localhost:6767/api)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
VERSION := "latest"
|
||||
IMG_NAME := "darkalex17/managarr"
|
||||
IMAGE := "{{IMG_NAME}}:{{VERSION}}"
|
||||
|
||||
|
||||
# List all recipes
|
||||
default:
|
||||
@@ -87,5 +85,5 @@ build build_type='debug':
|
||||
|
||||
# Build the docker image
|
||||
[group: 'build']
|
||||
build-docker:
|
||||
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMAGE}}
|
||||
build-docker version=VERSION:
|
||||
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMG_NAME}}:{{version}} .
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
tab_spaces=2
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
reorder_imports = true
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 319 KiB |
+126
-1
@@ -447,6 +447,78 @@ mod tests {
|
||||
assert_none!(config.port);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_bool() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: true
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_string() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: "true"
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_present() {
|
||||
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL", "true") };
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL}
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_defaults_to_false() {
|
||||
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY", "test") };
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY}
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &false);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_optional_env_var_bool_empty() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_none!(config.ssl);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_header_map_is_present() {
|
||||
@@ -507,6 +579,56 @@ mod tests {
|
||||
assert_none!(config.custom_headers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_string_vec_is_present() {
|
||||
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION", "/path1") };
|
||||
let expected_monitored_paths = ["/path1", "/path2"];
|
||||
let yaml_data = r#"
|
||||
monitored_storage_paths:
|
||||
- ${TEST_VAR_DESERIALIZE_STRING_VEC_OPTION}
|
||||
- /path2
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_string_vec_does_not_overwrite_non_env_value() {
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
"TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE",
|
||||
"/path3",
|
||||
)
|
||||
};
|
||||
let expected_monitored_paths = ["/path1", "/path2"];
|
||||
let yaml_data = r#"
|
||||
monitored_storage_paths:
|
||||
- /path1
|
||||
- /path2
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_optional_env_var_string_vec_empty() {
|
||||
let yaml_data = r#"
|
||||
api_token: "test123"
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_none!(config.monitored_storage_paths);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_u16_env_var_is_present() {
|
||||
@@ -620,10 +742,11 @@ mod tests {
|
||||
let api_token = "thisisatest".to_owned();
|
||||
let api_token_file = "/root/.config/api_token".to_owned();
|
||||
let ssl_cert_path = "/some/path".to_owned();
|
||||
let monitored_storage = vec!["/path1".to_owned(), "/path2".to_owned()];
|
||||
let mut custom_headers = HeaderMap::new();
|
||||
custom_headers.insert("X-Custom-Header", "value".parse().unwrap());
|
||||
let expected_str = format!(
|
||||
"ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}) }}"
|
||||
"ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl: Some(true), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}"
|
||||
);
|
||||
let servarr_config = ServarrConfig {
|
||||
name: Some(name),
|
||||
@@ -634,7 +757,9 @@ mod tests {
|
||||
api_token: Some(api_token),
|
||||
api_token_file: Some(api_token_file),
|
||||
ssl_cert_path: Some(ssl_cert_path),
|
||||
ssl: Some(true),
|
||||
custom_headers: Some(custom_headers),
|
||||
monitored_storage_paths: Some(monitored_storage),
|
||||
};
|
||||
|
||||
assert_str_eq!(format!("{servarr_config:?}"), expected_str);
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::models::Route;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS,
|
||||
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
||||
TRACK_DETAILS_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -103,8 +104,8 @@ pub static ALBUM_DETAILS_CONTEXT_CLUES: [ContextClue; 6] = [
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "episode details"),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
|
||||
(DEFAULT_KEYBINDINGS.submit, "track details"),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete track"),
|
||||
];
|
||||
|
||||
pub static ALBUM_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
|
||||
@@ -137,6 +138,26 @@ pub static MANUAL_ALBUM_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static TRACK_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static TRACK_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
|
||||
];
|
||||
|
||||
pub(in crate::app) struct LidarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for LidarrContextClueProvider {
|
||||
@@ -156,9 +177,20 @@ impl ContextClueProvider for LidarrContextClueProvider {
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.expect("album_details_modal is empty")
|
||||
.album_details_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.expect("album_details_modal is empty")
|
||||
.track_details_modal
|
||||
.as_ref()
|
||||
.expect("track_details_modal is empty")
|
||||
.track_details_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
ActiveLidarrBlock::AddArtistSearchInput
|
||||
| ActiveLidarrBlock::AddArtistEmptySearchResults
|
||||
| ActiveLidarrBlock::TestAllIndexers
|
||||
|
||||
@@ -10,13 +10,13 @@ mod tests {
|
||||
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ALBUM_DETAILS_CONTEXT_CLUES,
|
||||
ALBUM_HISTORY_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES,
|
||||
ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
|
||||
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
|
||||
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES, TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
INDEXER_SETTINGS_BLOCKS, LidarrData,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
|
||||
use crate::models::servarr_data::lidarr::modals::{AlbumDetailsModal, TrackDetailsModal};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use rstest::rstest;
|
||||
|
||||
@@ -266,11 +266,11 @@ mod tests {
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "episode details")
|
||||
&(DEFAULT_KEYBINDINGS.submit, "track details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete episode")
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete track")
|
||||
);
|
||||
assert_none!(album_details_context_clues_iter.next());
|
||||
}
|
||||
@@ -278,6 +278,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_album_history_context_clues() {
|
||||
let mut album_history_context_clues_iter = ALBUM_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
album_history_context_clues_iter.next(),
|
||||
&(
|
||||
@@ -348,6 +349,58 @@ mod tests {
|
||||
assert_none!(manual_album_search_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_context_clues() {
|
||||
let mut track_details_context_clues_iter = TRACK_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
track_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(track_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_history_context_clues() {
|
||||
let mut track_history_context_clues_iter = TRACK_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
|
||||
);
|
||||
assert_none!(track_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveLidarrBlock::ArtistHistory, &ARTIST_HISTORY_CONTEXT_CLUES)]
|
||||
@@ -391,6 +444,33 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::TrackDetails, &TRACK_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveLidarrBlock::TrackHistory, &TRACK_HISTORY_CONTEXT_CLUES)]
|
||||
fn test_lidarr_context_clue_provider_track_details_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
let mut track_details_modal = TrackDetailsModal::default();
|
||||
track_details_modal.track_details_tabs.set_index(index);
|
||||
let album_details_modal = AlbumDetailsModal {
|
||||
track_details_modal: Some(track_details_modal),
|
||||
..AlbumDetailsModal::default()
|
||||
};
|
||||
let lidarr_data = LidarrData {
|
||||
album_details_modal: Some(album_details_modal),
|
||||
..LidarrData::default()
|
||||
};
|
||||
app.data.lidarr_data = lidarr_data;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
@@ -7,7 +7,9 @@ mod tests {
|
||||
use crate::models::servarr_models::Indexer;
|
||||
use crate::network::NetworkEvent;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
album, artist, track,
|
||||
};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -55,6 +57,24 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_blocklist_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Blocklist)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetBlocklist.into());
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_artist_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
@@ -464,6 +484,46 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_track_details_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetTrackDetails(1).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_track_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackHistory)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetTrackHistory(1, 1, 1).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_lidarr_prompt_action_no_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
@@ -684,6 +744,32 @@ mod tests {
|
||||
assert_eq!(app.extract_artist_id().await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_album_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.albums.set_items(vec![album()]);
|
||||
|
||||
assert_eq!(app.extract_album_id().await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_track_id() {
|
||||
let mut app = App::test_default();
|
||||
let mut album_details_modal = AlbumDetailsModal::default();
|
||||
album_details_modal.tracks.set_items(vec![track()]);
|
||||
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
|
||||
|
||||
assert_eq!(app.extract_track_id().await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic(expected = "album_details_modal is empty")]
|
||||
async fn test_extract_track_id_panics_when_album_details_modal_is_not_set() {
|
||||
let app = App::test_default();
|
||||
|
||||
app.extract_track_id().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_lidarr_indexer_id() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
@@ -27,6 +27,11 @@ impl App<'_> {
|
||||
.dispatch_network_event(LidarrEvent::ListArtists.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::Blocklist => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetBlocklist.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::Downloads => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
|
||||
@@ -153,6 +158,23 @@ impl App<'_> {
|
||||
.dispatch_network_event(LidarrEvent::GetUpdates.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::TrackDetails => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::GetTrackDetails(self.extract_track_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::TrackHistory => {
|
||||
let artist_id = self.extract_artist_id().await;
|
||||
let album_id = self.extract_album_id().await;
|
||||
let track_id = self.extract_track_id().await;
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -179,6 +201,18 @@ impl App<'_> {
|
||||
self.data.lidarr_data.albums.current_selection().id
|
||||
}
|
||||
|
||||
async fn extract_track_id(&self) -> i64 {
|
||||
self
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.expect("album_details_modal is empty")
|
||||
.tracks
|
||||
.current_selection()
|
||||
.id
|
||||
}
|
||||
|
||||
async fn extract_lidarr_indexer_id(&self) -> i64 {
|
||||
self.data.lidarr_data.indexers.current_selection().id
|
||||
}
|
||||
|
||||
+54
-4
@@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use veil::Redact;
|
||||
|
||||
use crate::cli::Command;
|
||||
use crate::models::servarr_data::Notification;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
@@ -38,6 +39,7 @@ pub struct App<'a> {
|
||||
pub server_tabs: TabState,
|
||||
pub keymapping_table: Option<StatefulTable<KeybindingItem>>,
|
||||
pub error: HorizontallyScrollableText,
|
||||
pub notification: Option<Notification>,
|
||||
pub tick_until_poll: u64,
|
||||
pub ticks_until_scroll: u64,
|
||||
pub tick_count: u64,
|
||||
@@ -254,6 +256,7 @@ impl Default for App<'_> {
|
||||
cancellation_token: CancellationToken::new(),
|
||||
keymapping_table: None,
|
||||
error: HorizontallyScrollableText::default(),
|
||||
notification: None,
|
||||
is_first_render: true,
|
||||
server_tabs: TabState::new(Vec::new()),
|
||||
tick_until_poll: 400,
|
||||
@@ -346,11 +349,11 @@ pub struct AppConfig {
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn validate(&self) {
|
||||
pub fn validate(&self, config_path: &str) {
|
||||
if self.lidarr.is_none() && self.radarr.is_none() && self.sonarr.is_none() {
|
||||
log_and_print_error(
|
||||
"No Servarr configuration provided in the specified configuration file".to_owned(),
|
||||
);
|
||||
log_and_print_error(format!(
|
||||
"No Servarrs are configured in the file: {config_path}"
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
@@ -428,6 +431,8 @@ pub struct ServarrConfig {
|
||||
pub api_token: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
|
||||
pub api_token_file: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var_bool")]
|
||||
pub ssl: Option<bool>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
|
||||
pub ssl_cert_path: Option<String>,
|
||||
#[serde(
|
||||
@@ -436,6 +441,8 @@ pub struct ServarrConfig {
|
||||
serialize_with = "serialize_header_map"
|
||||
)]
|
||||
pub custom_headers: Option<HeaderMap>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var_string_vec")]
|
||||
pub monitored_storage_paths: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ServarrConfig {
|
||||
@@ -481,7 +488,9 @@ impl Default for ServarrConfig {
|
||||
api_token: Some(String::new()),
|
||||
api_token_file: None,
|
||||
ssl_cert_path: None,
|
||||
ssl: None,
|
||||
custom_headers: None,
|
||||
monitored_storage_paths: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -526,6 +535,29 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_env_var_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum StringOrBool {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
}
|
||||
|
||||
match StringOrBool::deserialize(deserializer)? {
|
||||
StringOrBool::Bool(b) => Ok(Some(b)),
|
||||
StringOrBool::String(s) => {
|
||||
let val = interpolate_env_vars(&s)
|
||||
.to_lowercase()
|
||||
.parse()
|
||||
.unwrap_or(false);
|
||||
Ok(Some(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_env_var_header_map<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<HeaderMap>, D::Error>
|
||||
@@ -548,6 +580,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_env_var_string_vec<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<String>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt: Option<Vec<String>> = Option::deserialize(deserializer)?;
|
||||
match opt {
|
||||
Some(vec) => Ok(Some(
|
||||
vec
|
||||
.into_iter()
|
||||
.map(|it| interpolate_env_vars(&it))
|
||||
.collect(),
|
||||
)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
|
||||
@@ -58,21 +58,19 @@ impl App<'_> {
|
||||
}
|
||||
ActiveSonarrBlock::SeasonHistory => {
|
||||
if !self.data.sonarr_data.seasons.is_empty() {
|
||||
let (series_id, season_number) = self.extract_series_id_season_number_tuple().await;
|
||||
self
|
||||
.dispatch_network_event(
|
||||
SonarrEvent::GetSeasonHistory(self.extract_series_id_season_number_tuple().await)
|
||||
.into(),
|
||||
)
|
||||
.dispatch_network_event(SonarrEvent::GetSeasonHistory(series_id, season_number).into())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ActiveSonarrBlock::ManualSeasonSearch => {
|
||||
match self.data.sonarr_data.season_details_modal.as_ref() {
|
||||
Some(season_details_modal) if season_details_modal.season_releases.is_empty() => {
|
||||
let (series_id, season_number) = self.extract_series_id_season_number_tuple().await;
|
||||
self
|
||||
.dispatch_network_event(
|
||||
SonarrEvent::GetSeasonReleases(self.extract_series_id_season_number_tuple().await)
|
||||
.into(),
|
||||
SonarrEvent::GetSeasonReleases(series_id, season_number).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ mod tests {
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
SonarrEvent::GetSeasonHistory((1, 1)).into()
|
||||
SonarrEvent::GetSeasonHistory(1, 1).into()
|
||||
);
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
@@ -175,7 +175,7 @@ mod tests {
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
SonarrEvent::GetSeasonReleases((1, 1)).into()
|
||||
SonarrEvent::GetSeasonReleases(1, 1).into()
|
||||
);
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
+36
-1
@@ -8,12 +8,18 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::lidarr::LidarrCommand;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
Cli,
|
||||
app::App,
|
||||
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{
|
||||
BlocklistItem as LidarrBlocklistItem, BlocklistResponse as LidarrBlocklistResponse,
|
||||
LidarrSerdeable,
|
||||
},
|
||||
radarr_models::{
|
||||
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
|
||||
RadarrSerdeable,
|
||||
@@ -182,5 +188,34 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
// TODO: Implement test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler
|
||||
#[tokio::test]
|
||||
async fn test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::BlocklistResponse(
|
||||
LidarrBlocklistResponse {
|
||||
records: vec![LidarrBlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let clear_blocklist_command = LidarrCommand::ClearBlocklist.into();
|
||||
|
||||
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand, arg};
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
@@ -28,6 +28,15 @@ pub enum LidarrDeleteCommand {
|
||||
#[arg(long, help = "Add a list exclusion for this album")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the specified item from the Lidarr blocklist")]
|
||||
BlocklistItem {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the blocklist item to remove from the blocklist",
|
||||
required = true
|
||||
)]
|
||||
blocklist_item_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the specified track file from disk")]
|
||||
TrackFile {
|
||||
#[arg(long, help = "The ID of the track file to delete", required = true)]
|
||||
@@ -107,6 +116,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteBlocklistItem(blocklist_item_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::TrackFile { track_file_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
|
||||
@@ -86,6 +86,42 @@ mod tests {
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "blocklist-item"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_success() {
|
||||
let expected_args = LidarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 1,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"blocklist-item",
|
||||
"--blocklist-item-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_track_file_requires_arguments() {
|
||||
let result =
|
||||
@@ -361,6 +397,37 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_blocklist_item_command() {
|
||||
let expected_blocklist_item_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_blocklist_item_command = LidarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 1,
|
||||
};
|
||||
|
||||
let result = LidarrDeleteCommandHandler::with(
|
||||
&app_arc,
|
||||
delete_blocklist_item_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_track_file_command() {
|
||||
let expected_track_file_id = 1;
|
||||
|
||||
@@ -44,6 +44,15 @@ pub enum LidarrGetCommand {
|
||||
SecurityConfig,
|
||||
#[command(about = "Get the system status")]
|
||||
SystemStatus,
|
||||
#[command(about = "Get detailed information for the track with the given ID")]
|
||||
TrackDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the track whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
track_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrGetCommand> for Command {
|
||||
@@ -115,6 +124,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHan
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::TrackDetails { track_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTrackDetails(track_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -106,6 +106,32 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_requires_track_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "track-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"get",
|
||||
"track-details",
|
||||
"--track-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
@@ -273,5 +299,31 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_track_details_command() {
|
||||
let expected_track_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetTrackDetails(expected_track_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_track_details_command = LidarrGetCommand::TrackDetails { track_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_track_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
fn test_commands_that_have_no_arg_requirements(
|
||||
#[values("test-all-indexers")] subcommand: &str,
|
||||
#[values("clear-blocklist", "test-all-indexers")] subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", subcommand]);
|
||||
|
||||
@@ -284,7 +284,9 @@ mod tests {
|
||||
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
|
||||
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
|
||||
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
|
||||
use crate::models::lidarr_models::{LidarrReleaseDownloadBody, LidarrTaskName};
|
||||
use crate::models::lidarr_models::{
|
||||
BlocklistItem, BlocklistResponse, LidarrReleaseDownloadBody, LidarrTaskName,
|
||||
};
|
||||
use crate::models::servarr_models::IndexerSettings;
|
||||
use crate::{
|
||||
app::App,
|
||||
@@ -546,6 +548,39 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_clear_blocklist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::BlocklistResponse(
|
||||
BlocklistResponse {
|
||||
records: vec![BlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let claer_blocklist_command = LidarrCommand::ClearBlocklist;
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_release_command() {
|
||||
let expected_release_download_body = LidarrReleaseDownloadBody {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, arg};
|
||||
use clap::Subcommand;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::LidarrCommand;
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "list_command_handler_tests.rs"]
|
||||
mod list_command_handler_tests;
|
||||
@@ -55,6 +57,10 @@ pub enum LidarrListCommand {
|
||||
},
|
||||
#[command(about = "List all artists in your Lidarr library")]
|
||||
Artists,
|
||||
#[command(about = "List all items in the Lidarr blocklist")]
|
||||
Blocklist,
|
||||
#[command(about = "List disk space details for all provisioned root folders in Lidarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "List all active downloads in Lidarr")]
|
||||
Downloads {
|
||||
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
|
||||
@@ -89,6 +95,27 @@ pub enum LidarrListCommand {
|
||||
Tags,
|
||||
#[command(about = "List all Lidarr tasks")]
|
||||
Tasks,
|
||||
#[command(about = "Fetch all history events for the track with the given ID")]
|
||||
TrackHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The artist ID that the track belongs to",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The album ID that the track is a part of",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the track whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
track_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "List the tracks for the album that corresponds to the artist with the given ID"
|
||||
)]
|
||||
@@ -177,6 +204,20 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Blocklist => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::DiskSpace => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetDiskSpace.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Downloads { count } => {
|
||||
let resp = self
|
||||
.network
|
||||
@@ -257,6 +298,27 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::TrackHistory {
|
||||
artist_id,
|
||||
album_id,
|
||||
track_id,
|
||||
} => {
|
||||
match self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into())
|
||||
.await
|
||||
{
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(history_vec))) => {
|
||||
let history_items_vec: Vec<LidarrHistoryItem> = history_vec
|
||||
.into_iter()
|
||||
.filter(|it| it.track_id == track_id)
|
||||
.collect();
|
||||
serde_json::to_string_pretty(&history_items_vec)?
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
|
||||
}
|
||||
}
|
||||
LidarrListCommand::Tracks {
|
||||
artist_id,
|
||||
album_id,
|
||||
|
||||
@@ -27,6 +27,8 @@ mod tests {
|
||||
fn test_list_commands_have_no_arg_requirements(
|
||||
#[values(
|
||||
"artists",
|
||||
"blocklist",
|
||||
"disk-space",
|
||||
"indexers",
|
||||
"metadata-profiles",
|
||||
"quality-profiles",
|
||||
@@ -225,6 +227,96 @@ mod tests {
|
||||
assert_eq!(logs_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_history_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-history",
|
||||
"--album-id",
|
||||
"1",
|
||||
"--track-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_history_requires_album_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--track-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_history_requires_track_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_history_success() {
|
||||
let expected_args = LidarrListCommand::TrackHistory {
|
||||
artist_id: 1,
|
||||
album_id: 1,
|
||||
track_id: 1,
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--album-id",
|
||||
"1",
|
||||
"--track-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(track_history_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(track_history_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tracks_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
@@ -325,6 +417,7 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -332,8 +425,9 @@ mod tests {
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::lidarr::list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::LidarrSerdeable;
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::lidarr_history_item;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
@@ -341,6 +435,8 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
#[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)]
|
||||
#[case(LidarrListCommand::Blocklist, LidarrEvent::GetBlocklist)]
|
||||
#[case(LidarrListCommand::DiskSpace, LidarrEvent::GetDiskSpace)]
|
||||
#[case(LidarrListCommand::Indexers, LidarrEvent::GetIndexers)]
|
||||
#[case(LidarrListCommand::MetadataProfiles, LidarrEvent::GetMetadataProfiles)]
|
||||
#[case(LidarrListCommand::QualityProfiles, LidarrEvent::GetQualityProfiles)]
|
||||
@@ -531,6 +627,49 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_track_history_command() {
|
||||
let expected_artist_id = 1;
|
||||
let expected_album_id = 1;
|
||||
let expected_track_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetTrackHistory(expected_artist_id, expected_album_id, expected_track_id)
|
||||
.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(
|
||||
vec![
|
||||
lidarr_history_item(),
|
||||
LidarrHistoryItem {
|
||||
track_id: 2,
|
||||
..lidarr_history_item()
|
||||
},
|
||||
],
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_track_history_command = LidarrListCommand::TrackHistory {
|
||||
artist_id: expected_artist_id,
|
||||
album_id: expected_album_id,
|
||||
track_id: expected_track_id,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrListCommandHandler::with(&app_arc, list_track_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&[lidarr_history_item()]).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_tracks_command() {
|
||||
let expected_artist_id = 1;
|
||||
|
||||
@@ -109,16 +109,20 @@ mod tests {
|
||||
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
|
||||
};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::LidarrSerdeable;
|
||||
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
torrent_release, usenet_release,
|
||||
};
|
||||
use crate::network::{MockNetworkTrait, NetworkEvent};
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manual_album_search_command() {
|
||||
let expected_releases = [torrent_release()];
|
||||
let expected_artist_id = 1;
|
||||
let expected_album_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
@@ -129,9 +133,13 @@ mod tests {
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
|
||||
torrent_release(),
|
||||
LidarrRelease {
|
||||
discography: true,
|
||||
..usenet_release()
|
||||
},
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_album_search_command = LidarrManualSearchCommand::Album {
|
||||
@@ -148,10 +156,18 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&expected_releases).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manual_discography_search_command() {
|
||||
let expected_releases = [LidarrRelease {
|
||||
discography: true,
|
||||
..usenet_release()
|
||||
}];
|
||||
let expected_artist_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
@@ -161,9 +177,13 @@ mod tests {
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
|
||||
torrent_release(),
|
||||
LidarrRelease {
|
||||
discography: true,
|
||||
..usenet_release()
|
||||
},
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_discography_search_command =
|
||||
@@ -178,6 +198,10 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&expected_releases).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, arg};
|
||||
use clap::Subcommand;
|
||||
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
|
||||
use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler};
|
||||
use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
|
||||
@@ -74,6 +74,8 @@ pub enum LidarrCommand {
|
||||
about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance"
|
||||
)]
|
||||
TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand),
|
||||
#[command(about = "Clear the Lidarr blocklist")]
|
||||
ClearBlocklist,
|
||||
#[command(about = "Manually download the given release")]
|
||||
DownloadRelease {
|
||||
#[arg(long, help = "The GUID of the release to download", required = true)]
|
||||
@@ -217,6 +219,17 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::ClearBlocklist => {
|
||||
self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ClearBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::DownloadRelease { guid, indexer_id } => {
|
||||
let params = LidarrReleaseDownloadBody { guid, indexer_id };
|
||||
let resp = self
|
||||
|
||||
+8
-1
@@ -1,8 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use clap_complete::Shell;
|
||||
use indoc::indoc;
|
||||
use lidarr::{LidarrCliHandler, LidarrCommand};
|
||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||
@@ -43,6 +44,12 @@ pub enum Command {
|
||||
#[arg(long, help = "Disable colored log output")]
|
||||
no_color: bool,
|
||||
},
|
||||
|
||||
#[command(about = indoc!{"
|
||||
Print the full path to the default configuration file.
|
||||
This file can be changed to another location using the '--config-file' flag
|
||||
"})]
|
||||
ConfigPath,
|
||||
}
|
||||
|
||||
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand, arg, command};
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::RadarrCommand;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -249,7 +249,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
|
||||
} => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetSeasonHistory((series_id, season_number)).into())
|
||||
.handle_network_event(SonarrEvent::GetSeasonHistory(series_id, season_number).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
|
||||
@@ -543,7 +543,7 @@ mod tests {
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetSeasonHistory((expected_series_id, expected_season_number)).into(),
|
||||
SonarrEvent::GetSeasonHistory(expected_series_id, expected_season_number).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
|
||||
@@ -30,7 +30,7 @@ pub enum SonarrManualSearchCommand {
|
||||
episode_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID"
|
||||
about = "Trigger a manual search of full-season releases (full_season: true) for the given season corresponding to the series with the given ID"
|
||||
)]
|
||||
Season {
|
||||
#[arg(
|
||||
@@ -75,11 +75,21 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
|
||||
let result = match self.command {
|
||||
SonarrManualSearchCommand::Episode { episode_id } => {
|
||||
println!("Searching for episode releases. This may take a minute...");
|
||||
let resp = self
|
||||
match self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetEpisodeReleases(episode_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
.await
|
||||
{
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => {
|
||||
let seasons_vec: Vec<SonarrRelease> = releases_vec
|
||||
.into_iter()
|
||||
.filter(|release| !release.full_season)
|
||||
.collect();
|
||||
serde_json::to_string_pretty(&seasons_vec)?
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
_ => serde_json::to_string_pretty(&json!({"message": "Unexpected response format"}))?,
|
||||
}
|
||||
}
|
||||
SonarrManualSearchCommand::Season {
|
||||
series_id,
|
||||
@@ -88,7 +98,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
|
||||
println!("Searching for season releases. This may take a minute...");
|
||||
match self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into())
|
||||
.handle_network_event(SonarrEvent::GetSeasonReleases(series_id, season_number).into())
|
||||
.await
|
||||
{
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => {
|
||||
|
||||
@@ -108,9 +108,13 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::models::sonarr_models::SonarrRelease;
|
||||
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
|
||||
torrent_release, usenet_release,
|
||||
};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
@@ -134,9 +138,13 @@ mod tests {
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![
|
||||
torrent_release(),
|
||||
SonarrRelease {
|
||||
full_season: true,
|
||||
..usenet_release()
|
||||
},
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 };
|
||||
@@ -150,23 +158,35 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&[torrent_release()]).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manual_season_search_command() {
|
||||
let expected_release = SonarrRelease {
|
||||
full_season: true,
|
||||
..usenet_release()
|
||||
};
|
||||
let expected_series_id = 1;
|
||||
let expected_season_number = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetSeasonReleases((expected_series_id, expected_season_number)).into(),
|
||||
SonarrEvent::GetSeasonReleases(expected_series_id, expected_season_number).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![
|
||||
torrent_release(),
|
||||
SonarrRelease {
|
||||
full_season: true,
|
||||
..usenet_release()
|
||||
},
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_season_search_command = SonarrManualSearchCommand::Season {
|
||||
@@ -183,6 +203,10 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&[expected_release]).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(
|
||||
SonarrEvent::ToggleSeasonMonitoring((series_id, season_number)).into(),
|
||||
SonarrEvent::ToggleSeasonMonitoring(series_id, season_number).into(),
|
||||
)
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
|
||||
@@ -755,7 +755,7 @@ mod tests {
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::ToggleSeasonMonitoring((expected_series_id, expected_season_number)).into(),
|
||||
SonarrEvent::ToggleSeasonMonitoring(expected_series_id, expected_season_number).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
|
||||
@@ -94,7 +94,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand>
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch((series_id, season_number)).into(),
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(series_id, season_number).into(),
|
||||
)
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
|
||||
@@ -197,7 +197,7 @@ mod tests {
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch((expected_series_id, expected_season_number))
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(expected_series_id, expected_season_number)
|
||||
.into(),
|
||||
))
|
||||
.times(1)
|
||||
|
||||
@@ -20,10 +20,10 @@ mod tests {
|
||||
use crate::handlers::{handle_events, populate_keymapping_table};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::servarr_data::{ActiveKeybindingBlock, Notification};
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
|
||||
@@ -174,6 +174,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_clear_notification() {
|
||||
let mut app = App::test_default();
|
||||
app.notification = Some(Notification::new(
|
||||
"Test".to_owned(),
|
||||
"Test".to_owned(),
|
||||
true,
|
||||
));
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
|
||||
|
||||
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
|
||||
|
||||
assert_none!(app.notification);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveRadarrBlock::MovieDetails.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
@@ -211,8 +231,8 @@ mod tests {
|
||||
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
|
||||
.iter()
|
||||
.map(|(key, desc)| {
|
||||
let (key, alt_key) = if key.alt.is_some() {
|
||||
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
|
||||
let (key, alt_key) = if let Some(key1) = key.alt {
|
||||
(key.key.to_string(), key1.to_string())
|
||||
} else {
|
||||
(key.key.to_string(), String::new())
|
||||
};
|
||||
@@ -284,9 +304,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_events_esc_clears_notification() {
|
||||
let mut app = App::test_default();
|
||||
app.notification = Some(Notification::new(
|
||||
"Download Result".to_owned(),
|
||||
"Download request sent successfully".to_owned(),
|
||||
true,
|
||||
));
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
|
||||
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
|
||||
|
||||
assert_none!(app.notification);
|
||||
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_events_esc_does_not_clear_notification_when_none() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
|
||||
|
||||
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
|
||||
|
||||
assert_none!(app.notification);
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
||||
}
|
||||
|
||||
fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem {
|
||||
let (key, alt_key) = if key.alt.is_some() {
|
||||
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
|
||||
let (key, alt_key) = if let Some(key1) = key.alt {
|
||||
(key.key.to_string(), key1.to_string())
|
||||
} else {
|
||||
(key.key.to_string(), String::new())
|
||||
};
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use chrono::DateTime;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::blocklist::{BlocklistHandler, blocklist_sorting_options};
|
||||
use crate::models::lidarr_models::{Artist, BlocklistItem};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::servarr_models::{Quality, QualityWrapper};
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist;
|
||||
|
||||
mod test_handle_delete {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_prompt() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
BlocklistHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteBlocklistItemPrompt.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
BlocklistHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Blocklist.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_pushed;
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_tab_left(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(2);
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.main_tabs.get_active_route(),
|
||||
ActiveLidarrBlock::Downloads.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_tab_right(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(2);
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.main_tabs.get_active_route(),
|
||||
ActiveLidarrBlock::History.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::History.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_left_right_prompt_toggle(
|
||||
#[values(
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt,
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(key, &mut app, active_lidarr_block, None).handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
|
||||
BlocklistHandler::new(key, &mut app, active_lidarr_block, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::BlocklistItemDetails.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Blocklist.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt,
|
||||
LidarrEvent::DeleteBlocklistItem(3)
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt,
|
||||
LidarrEvent::ClearBlocklist
|
||||
)]
|
||||
fn test_blocklist_prompt_confirm_submit(
|
||||
#[case] base_route: ActiveLidarrBlock,
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
#[case] expected_action: LidarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
assert_navigation_popped!(app, base_route.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_prompt_decline_submit(
|
||||
#[values(
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt,
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt
|
||||
)]
|
||||
prompt_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Blocklist.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt
|
||||
)]
|
||||
fn test_blocklist_prompt_blocks_esc(
|
||||
#[case] base_block: ActiveLidarrBlock,
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(base_block.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
|
||||
BlocklistHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, base_block.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_esc_blocklist_item_details() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::BlocklistItemDetails.into());
|
||||
|
||||
BlocklistHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::BlocklistItemDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Blocklist.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_default_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.error = "test error".to_owned().into();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::Blocklist.into());
|
||||
assert_is_empty!(app.error.text);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
use super::*;
|
||||
use crate::{assert_navigation_popped, assert_navigation_pushed};
|
||||
|
||||
#[test]
|
||||
fn test_refresh_blocklist_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::Blocklist.into());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_blocklist_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Blocklist.into());
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_blocklist_key() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.clear.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::BlocklistClearAllItemsPrompt.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_blocklist_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.clear.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Blocklist.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt,
|
||||
LidarrEvent::DeleteBlocklistItem(3)
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt,
|
||||
LidarrEvent::ClearBlocklist
|
||||
)]
|
||||
fn test_blocklist_prompt_confirm(
|
||||
#[case] base_route: ActiveLidarrBlock,
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
#[case] expected_action: LidarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
prompt_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.lidarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
assert_navigation_popped!(app, base_route.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_sorting_options_artist_name() {
|
||||
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
|
||||
a.artist
|
||||
.artist_name
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.artist.artist_name.text.to_lowercase())
|
||||
};
|
||||
let mut expected_blocklist_vec = blocklist_vec();
|
||||
expected_blocklist_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = blocklist_sorting_options()[0].clone();
|
||||
let mut sorted_blocklist_vec = blocklist_vec();
|
||||
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
|
||||
assert_str_eq!(sort_option.name, "Artist Name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_sorting_options_source_title() {
|
||||
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
};
|
||||
let mut expected_blocklist_vec = blocklist_vec();
|
||||
expected_blocklist_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = blocklist_sorting_options()[1].clone();
|
||||
let mut sorted_blocklist_vec = blocklist_vec();
|
||||
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
|
||||
assert_str_eq!(sort_option.name, "Source Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_sorting_options_quality() {
|
||||
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
|
||||
a.quality
|
||||
.quality
|
||||
.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.quality.quality.name.to_lowercase())
|
||||
};
|
||||
let mut expected_blocklist_vec = blocklist_vec();
|
||||
expected_blocklist_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = blocklist_sorting_options()[2].clone();
|
||||
let mut sorted_blocklist_vec = blocklist_vec();
|
||||
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
|
||||
assert_str_eq!(sort_option.name, "Quality");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_sorting_options_date() {
|
||||
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering =
|
||||
|a, b| a.date.cmp(&b.date);
|
||||
let mut expected_blocklist_vec = blocklist_vec();
|
||||
expected_blocklist_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = blocklist_sorting_options()[3].clone();
|
||||
let mut sorted_blocklist_vec = blocklist_vec();
|
||||
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
|
||||
assert_str_eq!(sort_option.name, "Date");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_handler_accepts() {
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if BLOCKLIST_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(BlocklistHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!BlocklistHandler::accepts(active_lidarr_block));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
let handler = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_blocklist_item_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
let blocklist_item_id = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.extract_blocklist_item_id();
|
||||
|
||||
assert_eq!(blocklist_item_id, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_handler_not_ready_when_blocklist_is_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_handler_ready_when_not_loading_and_blocklist_is_not_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
|
||||
app.is_loading = false;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.blocklist
|
||||
.set_items(vec![BlocklistItem::default()]);
|
||||
|
||||
let handler = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
fn blocklist_vec() -> Vec<BlocklistItem> {
|
||||
vec![
|
||||
BlocklistItem {
|
||||
id: 3,
|
||||
source_title: "test 1".to_owned(),
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "Lossless".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
|
||||
artist: Artist {
|
||||
artist_name: "test 3".into(),
|
||||
..artist()
|
||||
},
|
||||
..BlocklistItem::default()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 2,
|
||||
source_title: "test 2".to_owned(),
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "Lossy".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
|
||||
artist: Artist {
|
||||
artist_name: "test 2".into(),
|
||||
..artist()
|
||||
},
|
||||
..BlocklistItem::default()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 1,
|
||||
source_title: "test 3".to_owned(),
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "Lossless".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
|
||||
artist: Artist {
|
||||
artist_name: "".into(),
|
||||
..artist()
|
||||
},
|
||||
..BlocklistItem::default()
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::BlocklistItem;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "blocklist_handler_tests.rs"]
|
||||
mod blocklist_handler_tests;
|
||||
|
||||
pub(super) struct BlocklistHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl BlocklistHandler<'_, '_> {
|
||||
fn extract_blocklist_item_id(&self) -> i64 {
|
||||
self.app.data.lidarr_data.blocklist.current_selection().id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for BlocklistHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let blocklist_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveLidarrBlock::Blocklist.into())
|
||||
.sorting_block(ActiveLidarrBlock::BlocklistSortPrompt.into())
|
||||
.sort_options(blocklist_sorting_options());
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.lidarr_data.blocklist,
|
||||
blocklist_table_handling_config,
|
||||
) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
BLOCKLIST_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
) -> Self {
|
||||
BlocklistHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
_context: context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading && !self.app.data.lidarr_data.blocklist.is_empty()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::Blocklist {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::DeleteBlocklistItemPrompt.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::Blocklist => handle_change_tab_left_right_keys(self.app, self.key),
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt
|
||||
| ActiveLidarrBlock::BlocklistClearAllItemsPrompt => handle_prompt_toggle(self.app, self.key),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::DeleteBlocklistItem(
|
||||
self.extract_blocklist_item_id(),
|
||||
));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt => {
|
||||
if self.app.data.lidarr_data.prompt_confirm {
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::ClearBlocklist);
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveLidarrBlock::Blocklist => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::BlocklistItemDetails.into());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt
|
||||
| ActiveLidarrBlock::BlocklistClearAllItemsPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.prompt_confirm = false;
|
||||
}
|
||||
ActiveLidarrBlock::BlocklistItemDetails | ActiveLidarrBlock::BlocklistSortPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => handle_clear_errors(self.app),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::Blocklist => match self.key {
|
||||
_ if matches_key!(refresh, key) => {
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
_ if matches_key!(clear, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::BlocklistClearAllItemsPrompt.into());
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::DeleteBlocklistItem(
|
||||
self.extract_blocklist_item_id(),
|
||||
));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::ClearBlocklist);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
fn blocklist_sorting_options() -> Vec<SortOption<BlocklistItem>> {
|
||||
vec![
|
||||
SortOption {
|
||||
name: "Artist Name",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.artist
|
||||
.artist_name
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.artist.artist_name.text.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Quality",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.quality
|
||||
.quality
|
||||
.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.quality.quality.name.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Date",
|
||||
cmp_fn: Some(|a, b| a.date.cmp(&b.date)),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -99,9 +99,9 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.main_tabs.get_active_route(),
|
||||
ActiveLidarrBlock::History.into()
|
||||
ActiveLidarrBlock::Blocklist.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::History.into());
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::Blocklist.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
||||
@@ -29,7 +29,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::History.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(2);
|
||||
app.data.lidarr_data.main_tabs.set_index(3);
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
@@ -41,9 +41,9 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.main_tabs.get_active_route(),
|
||||
ActiveLidarrBlock::Downloads.into()
|
||||
ActiveLidarrBlock::Blocklist.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into());
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::Blocklist.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::History.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(2);
|
||||
app.data.lidarr_data.main_tabs.set_index(3);
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
|
||||
@@ -506,10 +506,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditIndexerHandler<'
|
||||
.tags
|
||||
);
|
||||
}
|
||||
ActiveLidarrBlock::EditIndexerPrompt => {
|
||||
ActiveLidarrBlock::EditIndexerPrompt
|
||||
if self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::EditIndexerConfirmPrompt
|
||||
&& matches_key!(confirm, self.key)
|
||||
&& matches_key!(confirm, self.key) =>
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
@@ -518,7 +518,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditIndexerHandler<'
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +106,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for IndexerSettingsHandl
|
||||
indexer_settings.maximum_size -= 1;
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => {
|
||||
if indexer_settings.rss_sync_interval > 0 {
|
||||
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput
|
||||
if indexer_settings.rss_sync_interval > 0 =>
|
||||
{
|
||||
indexer_settings.rss_sync_interval -= 1;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(4);
|
||||
app.data.lidarr_data.main_tabs.set_index(5);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
@@ -89,7 +89,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Indexers.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(4);
|
||||
app.data.lidarr_data.main_tabs.set_index(5);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
|
||||
@@ -591,17 +591,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a,
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistPrompt => {
|
||||
ActiveLidarrBlock::AddArtistPrompt
|
||||
if self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::AddArtistConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
&& matches_key!(confirm, key) =>
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::AddArtist(self.build_add_artist_body()));
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,22 +227,22 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AlbumDetailsHandler<
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
// ActiveLidarrBlock::AlbumDetails
|
||||
// if self.app.data.lidarr_data.album_details_modal.is_some()
|
||||
// && !self
|
||||
// .app
|
||||
// .data
|
||||
// .lidarr_data
|
||||
// .album_details_modal
|
||||
// .as_ref()
|
||||
// .unwrap()
|
||||
// .tracks
|
||||
// .is_empty() =>
|
||||
// {
|
||||
// self
|
||||
// .app
|
||||
// .push_navigation_stack(ActiveLidarrBlock::TrackDetails.into())
|
||||
// }
|
||||
ActiveLidarrBlock::AlbumDetails
|
||||
if self.app.data.lidarr_data.album_details_modal.is_some()
|
||||
&& !self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.tracks
|
||||
.is_empty() =>
|
||||
{
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into())
|
||||
}
|
||||
ActiveLidarrBlock::AlbumHistory => self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::AlbumHistoryDetails.into()),
|
||||
|
||||
@@ -155,37 +155,37 @@ mod tests {
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
// #[test]
|
||||
// fn test_album_details_submit() {
|
||||
// let mut app = App::test_default_fully_populated();
|
||||
// app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||
//
|
||||
// AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None)
|
||||
// .handle();
|
||||
//
|
||||
// assert_navigation_pushed!(app, ActiveLidarrBlock::TrackDetails.into());
|
||||
// }
|
||||
#[test]
|
||||
fn test_album_details_submit() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||
|
||||
// #[test]
|
||||
// fn test_album_details_submit_no_op_on_empty_tracks_table() {
|
||||
// let mut app = App::test_default_fully_populated();
|
||||
// app
|
||||
// .data
|
||||
// .lidarr_data
|
||||
// .album_details_modal
|
||||
// .as_mut()
|
||||
// .unwrap()
|
||||
// .tracks = StatefulTable::default();
|
||||
// app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||
//
|
||||
// AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None)
|
||||
// .handle();
|
||||
//
|
||||
// assert_eq!(
|
||||
// app.get_current_route(),
|
||||
// ActiveLidarrBlock::AlbumDetails.into()
|
||||
// );
|
||||
// }
|
||||
AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::TrackDetails.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_details_submit_no_op_on_empty_tracks_table() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tracks = StatefulTable::default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||
|
||||
AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::AlbumDetails.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_details_submit_no_op_when_not_ready() {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::lidarr_handlers::history::history_sorting_options;
|
||||
use crate::handlers::lidarr_handlers::library::album_details_handler::AlbumDetailsHandler;
|
||||
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
@@ -26,7 +24,7 @@ pub struct ArtistDetailsHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl ArtistDetailsHandler<'_, '_> {
|
||||
@@ -76,24 +74,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|
||||
|app| &mut app.data.lidarr_data.discography_releases,
|
||||
artist_releases_table_handling_config,
|
||||
) {
|
||||
match self.active_lidarr_block {
|
||||
_ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => {
|
||||
DeleteAlbumHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if AlbumDetailsHandler::accepts(self.active_lidarr_block) => {
|
||||
AlbumDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
};
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
DeleteAlbumHandler::accepts(active_block)
|
||||
|| AlbumDetailsHandler::accepts(active_block)
|
||||
|| ARTIST_DETAILS_BLOCKS.contains(&active_block)
|
||||
ARTIST_DETAILS_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
@@ -104,13 +90,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
) -> ArtistDetailsHandler<'a, 'b> {
|
||||
ArtistDetailsHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
context,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,8 +293,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
|
||||
}
|
||||
_ if matches_key!(toggle_monitoring, key) => {
|
||||
if !self.app.data.lidarr_data.albums.is_empty() {
|
||||
_ if matches_key!(toggle_monitoring, key)
|
||||
&& !self.app.data.lidarr_data.albums.is_empty() =>
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::ToggleAlbumMonitoring(self.extract_album_id()));
|
||||
@@ -317,7 +304,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveLidarrBlock::ArtistHistory | ActiveLidarrBlock::ManualArtistSearch => match self.key {
|
||||
|
||||
@@ -13,12 +13,11 @@ mod tests {
|
||||
ArtistDetailsHandler, releases_sorting_options,
|
||||
};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease};
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS,
|
||||
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
|
||||
};
|
||||
use crate::models::servarr_models::{Quality, QualityWrapper};
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_delete {
|
||||
use super::*;
|
||||
@@ -812,12 +811,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_handler_accepts() {
|
||||
let mut artist_details_blocks = ARTIST_DETAILS_BLOCKS.clone().to_vec();
|
||||
artist_details_blocks.extend(DELETE_ALBUM_BLOCKS);
|
||||
artist_details_blocks.extend(ALBUM_DETAILS_BLOCKS);
|
||||
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if artist_details_blocks.contains(&active_lidarr_block) {
|
||||
if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(ArtistDetailsHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!ArtistDetailsHandler::accepts(active_lidarr_block));
|
||||
@@ -977,58 +972,6 @@ mod tests {
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegates_delete_album_blocks_to_delete_album_handler() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.albums
|
||||
.set_items(vec![Album::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::ArtistDetails.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_album_details_blocks_to_album_details_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AlbumDetails,
|
||||
ActiveLidarrBlock::AlbumHistory,
|
||||
ActiveLidarrBlock::SearchTracks,
|
||||
ActiveLidarrBlock::SearchTracksError,
|
||||
ActiveLidarrBlock::AutomaticallySearchAlbumPrompt,
|
||||
ActiveLidarrBlock::SearchAlbumHistory,
|
||||
ActiveLidarrBlock::SearchAlbumHistoryError,
|
||||
ActiveLidarrBlock::FilterAlbumHistory,
|
||||
ActiveLidarrBlock::FilterAlbumHistoryError,
|
||||
ActiveLidarrBlock::AlbumHistorySortPrompt,
|
||||
ActiveLidarrBlock::AlbumHistoryDetails,
|
||||
ActiveLidarrBlock::ManualAlbumSearch,
|
||||
ActiveLidarrBlock::ManualAlbumSearchSortPrompt,
|
||||
ActiveLidarrBlock::DeleteTrackFilePrompt
|
||||
)]
|
||||
active_sonarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
test_handler_delegation!(
|
||||
ArtistDetailsHandler,
|
||||
ActiveLidarrBlock::Artists,
|
||||
active_sonarr_block
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_source() {
|
||||
let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering =
|
||||
|
||||
@@ -428,10 +428,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditArtistHandler<'a
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::EditArtistPrompt => {
|
||||
ActiveLidarrBlock::EditArtistPrompt
|
||||
if self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::EditArtistConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
&& matches_key!(confirm, key) =>
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
@@ -440,7 +440,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditArtistHandler<'a
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ mod tests {
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
|
||||
DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
LIBRARY_BLOCKS,
|
||||
LIBRARY_BLOCKS, TRACK_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
@@ -34,10 +34,14 @@ mod tests {
|
||||
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
|
||||
library_handler_blocks.extend(ADD_ARTIST_BLOCKS);
|
||||
library_handler_blocks.extend(ALBUM_DETAILS_BLOCKS);
|
||||
library_handler_blocks.extend(TRACK_DETAILS_BLOCKS);
|
||||
|
||||
ActiveLidarrBlock::iter().for_each(|lidarr_block| {
|
||||
if library_handler_blocks.contains(&lidarr_block) {
|
||||
assert!(LibraryHandler::accepts(lidarr_block));
|
||||
assert!(
|
||||
LibraryHandler::accepts(lidarr_block),
|
||||
"{lidarr_block} is not accepted by the LibraryHandler"
|
||||
);
|
||||
} else {
|
||||
assert!(!LibraryHandler::accepts(lidarr_block));
|
||||
}
|
||||
@@ -670,6 +674,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_track_details_blocks_to_track_details_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::TrackDetails,
|
||||
ActiveLidarrBlock::TrackHistory,
|
||||
ActiveLidarrBlock::TrackHistoryDetails,
|
||||
ActiveLidarrBlock::SearchTrackHistory,
|
||||
ActiveLidarrBlock::SearchTrackHistoryError,
|
||||
ActiveLidarrBlock::FilterTrackHistory,
|
||||
ActiveLidarrBlock::FilterTrackHistoryError,
|
||||
ActiveLidarrBlock::TrackHistorySortPrompt
|
||||
)]
|
||||
active_sonarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
test_handler_delegation!(
|
||||
LibraryHandler,
|
||||
ActiveLidarrBlock::AlbumDetails,
|
||||
active_sonarr_block
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_key() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
@@ -19,18 +19,22 @@ use super::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
|
||||
mod add_artist_handler;
|
||||
mod album_details_handler;
|
||||
mod artist_details_handler;
|
||||
mod delete_album_handler;
|
||||
mod delete_artist_handler;
|
||||
mod edit_artist_handler;
|
||||
mod track_details_handler;
|
||||
|
||||
use crate::handlers::lidarr_handlers::library::album_details_handler::AlbumDetailsHandler;
|
||||
use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler;
|
||||
use crate::handlers::lidarr_handlers::library::track_details_handler::TrackDetailsHandler;
|
||||
use crate::models::Route;
|
||||
pub(in crate::handlers::lidarr_handlers) use add_artist_handler::AddArtistHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use artist_details_handler::ArtistDetailsHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler;
|
||||
pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler;
|
||||
|
||||
mod album_details_handler;
|
||||
#[cfg(test)]
|
||||
#[path = "library_handler_tests.rs"]
|
||||
mod library_handler_tests;
|
||||
@@ -82,6 +86,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
|
||||
ArtistDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => {
|
||||
DeleteAlbumHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if AlbumDetailsHandler::accepts(self.active_lidarr_block) => {
|
||||
AlbumDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if TrackDetailsHandler::accepts(self.active_lidarr_block) => {
|
||||
TrackDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
}
|
||||
}
|
||||
@@ -90,8 +106,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, '
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
AddArtistHandler::accepts(active_block)
|
||||
|| DeleteArtistHandler::accepts(active_block)
|
||||
|| DeleteAlbumHandler::accepts(active_block)
|
||||
|| EditArtistHandler::accepts(active_block)
|
||||
|| ArtistDetailsHandler::accepts(active_block)
|
||||
|| AlbumDetailsHandler::accepts(active_block)
|
||||
|| TrackDetailsHandler::accepts(active_block)
|
||||
|| LIBRARY_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::history::history_sorting_options;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::LidarrHistoryItem;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "track_details_handler_tests.rs"]
|
||||
mod track_details_handler_tests;
|
||||
|
||||
pub(super) struct TrackDetailsHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for TrackDetailsHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let track_history_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveLidarrBlock::TrackHistory.into())
|
||||
.sorting_block(ActiveLidarrBlock::TrackHistorySortPrompt.into())
|
||||
.sort_options(history_sorting_options())
|
||||
.searching_block(ActiveLidarrBlock::SearchTrackHistory.into())
|
||||
.search_error_block(ActiveLidarrBlock::SearchTrackHistoryError.into())
|
||||
.search_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text)
|
||||
.filtering_block(ActiveLidarrBlock::FilterTrackHistory.into())
|
||||
.filter_error_block(ActiveLidarrBlock::FilterTrackHistoryError.into())
|
||||
.filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text);
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| {
|
||||
&mut app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.expect("Album details modal is undefined")
|
||||
.track_details_modal
|
||||
.as_mut()
|
||||
.expect("Track details modal is undefined")
|
||||
.track_history
|
||||
},
|
||||
track_history_table_handling_config,
|
||||
) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
TRACK_DETAILS_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
) -> Self {
|
||||
Self {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
if self.app.is_loading {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(album_details_modal) = self.app.data.lidarr_data.album_details_modal.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(track_details_modal) = &album_details_modal.track_details_modal else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::TrackDetails => !track_details_modal.track_details.is_empty(),
|
||||
ActiveLidarrBlock::TrackHistory => !track_details_modal.track_history.is_empty(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => match self.key {
|
||||
_ if matches_key!(left, self.key) => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_tabs
|
||||
.previous();
|
||||
self.app.pop_and_push_navigation_stack(
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_tabs
|
||||
.get_active_route(),
|
||||
);
|
||||
}
|
||||
_ if matches_key!(right, self.key) => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_tabs
|
||||
.next();
|
||||
self.app.pop_and_push_navigation_stack(
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_tabs
|
||||
.get_active_route(),
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::TrackHistory {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::TrackHistoryDetails.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => {
|
||||
self.app.pop_navigation_stack();
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_modal = None;
|
||||
}
|
||||
ActiveLidarrBlock::TrackHistoryDetails => {
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => match self.key {
|
||||
_ if matches_key!(refresh, self.key) => {
|
||||
self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::track_details_handler::TrackDetailsHandler;
|
||||
use crate::models::ScrollableText;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
mod test_handle_left_right_actions {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
|
||||
#[case(ActiveLidarrBlock::TrackHistory, ActiveLidarrBlock::TrackDetails)]
|
||||
fn test_track_details_tabs_left_right_action(
|
||||
#[case] left_block: ActiveLidarrBlock,
|
||||
#[case] right_block: ActiveLidarrBlock,
|
||||
#[values(true, false)] is_ready: bool,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(right_block.into());
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_tabs
|
||||
.index = app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_tabs
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|tab_route| tab_route.route == right_block.into())
|
||||
.unwrap_or_default();
|
||||
|
||||
TrackDetailsHandler::new(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_tabs
|
||||
.get_active_route()
|
||||
);
|
||||
assert_navigation_pushed!(app, left_block.into());
|
||||
|
||||
TrackDetailsHandler::new(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_tabs
|
||||
.get_active_route()
|
||||
);
|
||||
assert_navigation_pushed!(app, right_block.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use super::*;
|
||||
use crate::event::Key;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_track_history_submit() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
|
||||
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::TrackHistoryDetails.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_history_submit_no_op_when_track_history_is_empty() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_history = StatefulTable::default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
|
||||
|
||||
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::TrackHistory.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_history_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
|
||||
|
||||
TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::TrackHistory.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::event::Key;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[test]
|
||||
fn test_track_history_details_block_esc() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackHistoryDetails.into());
|
||||
|
||||
TrackDetailsHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::TrackHistoryDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::TrackHistory.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_track_details_tabs_esc(
|
||||
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
TrackDetailsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::AlbumDetails.into());
|
||||
assert_none!(
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[rstest]
|
||||
fn test_refresh_key(
|
||||
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.is_routing = false;
|
||||
|
||||
TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, active_lidarr_block.into());
|
||||
assert!(app.is_routing);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_refresh_key_no_op_when_not_ready(
|
||||
#[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.is_routing = false;
|
||||
|
||||
TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_handler_accepts() {
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(TrackDetailsHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!TrackDetailsHandler::accepts(active_lidarr_block));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_track_details_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
let handler = TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_handler_is_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::TrackDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_handler_is_not_ready_when_album_details_modal_is_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::TrackDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_handler_is_not_ready_when_track_details_modal_is_empty() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_modal = None;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::TrackDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_handler_is_not_ready_when_track_details_is_empty() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details = ScrollableText::default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::TrackDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_handler_is_not_ready_when_track_history_table_is_empty() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.track_history = StatefulTable::default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::TrackHistory,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_track_details_handler_is_ready(
|
||||
#[values(
|
||||
ActiveLidarrBlock::TrackDetails,
|
||||
ActiveLidarrBlock::TrackHistory,
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
active_lidarr_block,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,12 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)]
|
||||
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
|
||||
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)]
|
||||
#[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)]
|
||||
#[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)]
|
||||
#[case(5, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)]
|
||||
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Blocklist)]
|
||||
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::History)]
|
||||
#[case(3, ActiveLidarrBlock::Blocklist, ActiveLidarrBlock::RootFolders)]
|
||||
#[case(4, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)]
|
||||
#[case(5, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)]
|
||||
#[case(6, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)]
|
||||
fn test_lidarr_handler_change_tab_left_right_keys(
|
||||
#[case] index: usize,
|
||||
#[case] left_block: ActiveLidarrBlock,
|
||||
@@ -87,11 +88,12 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)]
|
||||
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
|
||||
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)]
|
||||
#[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)]
|
||||
#[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)]
|
||||
#[case(5, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)]
|
||||
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Blocklist)]
|
||||
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::History)]
|
||||
#[case(3, ActiveLidarrBlock::Blocklist, ActiveLidarrBlock::RootFolders)]
|
||||
#[case(4, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)]
|
||||
#[case(5, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)]
|
||||
#[case(6, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)]
|
||||
fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation(
|
||||
#[case] index: usize,
|
||||
#[case] left_block: ActiveLidarrBlock,
|
||||
@@ -122,10 +124,11 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::Artists)]
|
||||
#[case(1, ActiveLidarrBlock::Downloads)]
|
||||
#[case(2, ActiveLidarrBlock::History)]
|
||||
#[case(3, ActiveLidarrBlock::RootFolders)]
|
||||
#[case(4, ActiveLidarrBlock::Indexers)]
|
||||
#[case(5, ActiveLidarrBlock::System)]
|
||||
#[case(2, ActiveLidarrBlock::Blocklist)]
|
||||
#[case(3, ActiveLidarrBlock::History)]
|
||||
#[case(4, ActiveLidarrBlock::RootFolders)]
|
||||
#[case(5, ActiveLidarrBlock::Indexers)]
|
||||
#[case(6, ActiveLidarrBlock::System)]
|
||||
fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key(
|
||||
#[case] index: usize,
|
||||
#[case] block: ActiveLidarrBlock,
|
||||
@@ -197,6 +200,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_blocklist_blocks_to_blocklist_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::BlocklistItemDetails,
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt,
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt,
|
||||
ActiveLidarrBlock::BlocklistSortPrompt
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
test_handler_delegation!(
|
||||
LidarrHandler,
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
active_lidarr_block
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_history_blocks_to_history_handler(
|
||||
#[values(
|
||||
|
||||
@@ -3,6 +3,7 @@ use indexers::IndexersHandler;
|
||||
use library::LibraryHandler;
|
||||
|
||||
use super::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::blocklist::BlocklistHandler;
|
||||
use crate::handlers::lidarr_handlers::downloads::DownloadsHandler;
|
||||
use crate::handlers::lidarr_handlers::root_folders::RootFoldersHandler;
|
||||
use crate::handlers::lidarr_handlers::system::SystemHandler;
|
||||
@@ -11,6 +12,7 @@ use crate::{
|
||||
app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
|
||||
};
|
||||
|
||||
mod blocklist;
|
||||
mod downloads;
|
||||
mod history;
|
||||
mod indexers;
|
||||
@@ -38,6 +40,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b
|
||||
_ if DownloadsHandler::accepts(self.active_lidarr_block) => {
|
||||
DownloadsHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
|
||||
}
|
||||
_ if BlocklistHandler::accepts(self.active_lidarr_block) => {
|
||||
BlocklistHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
|
||||
}
|
||||
_ if HistoryHandler::accepts(self.active_lidarr_block) => {
|
||||
HistoryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
|
||||
}
|
||||
|
||||
@@ -505,10 +505,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddRootFolderHandler
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveLidarrBlock::AddRootFolderPrompt => {
|
||||
ActiveLidarrBlock::AddRootFolderPrompt
|
||||
if self.app.data.lidarr_data.selected_block.get_active_block()
|
||||
== ActiveLidarrBlock::AddRootFolderConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
&& matches_key!(confirm, key) =>
|
||||
{
|
||||
self.app.data.lidarr_data.prompt_confirm = true;
|
||||
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder(
|
||||
@@ -518,7 +518,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddRootFolderHandler
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(3);
|
||||
app.data.lidarr_data.main_tabs.set_index(4);
|
||||
|
||||
RootFoldersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
@@ -93,7 +93,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(3);
|
||||
app.data.lidarr_data.main_tabs.set_index(4);
|
||||
|
||||
RootFoldersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
|
||||
@@ -27,7 +27,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::System.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(5);
|
||||
app.data.lidarr_data.main_tabs.set_index(6);
|
||||
|
||||
SystemHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
@@ -49,7 +49,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::System.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.lidarr_data.main_tabs.set_index(5);
|
||||
app.data.lidarr_data.main_tabs.set_index(6);
|
||||
|
||||
SystemHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
|
||||
+4
-2
@@ -116,6 +116,8 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
||||
} else {
|
||||
app.keymapping_table = None;
|
||||
}
|
||||
} else if matches_key!(esc, key) && app.notification.is_some() {
|
||||
app.notification.take();
|
||||
} else {
|
||||
match app.get_current_route() {
|
||||
_ if app.keymapping_table.is_some() => {
|
||||
@@ -137,8 +139,8 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
||||
|
||||
pub fn populate_keymapping_table(app: &mut App<'_>) {
|
||||
let context_clue_to_keybinding_item = |key: &KeyBinding, desc: &&str| {
|
||||
let (key, alt_key) = if key.alt.is_some() {
|
||||
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
|
||||
let (key, alt_key) = if let Some(key1) = key.alt {
|
||||
(key.key.to_string(), key1.to_string())
|
||||
} else {
|
||||
(key.key.to_string(), String::new())
|
||||
};
|
||||
|
||||
@@ -354,10 +354,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
|
||||
.path
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::EditCollectionPrompt => {
|
||||
ActiveRadarrBlock::EditCollectionPrompt
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== ActiveRadarrBlock::EditCollectionConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
&& matches_key!(confirm, key) =>
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(
|
||||
@@ -367,7 +367,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,10 +507,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
|
||||
.tags
|
||||
);
|
||||
}
|
||||
ActiveRadarrBlock::EditIndexerPrompt => {
|
||||
ActiveRadarrBlock::EditIndexerPrompt
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== ActiveRadarrBlock::EditIndexerConfirmPrompt
|
||||
&& matches_key!(confirm, self.key)
|
||||
&& matches_key!(confirm, self.key) =>
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action =
|
||||
@@ -519,7 +519,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,11 +114,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
|
||||
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput => {
|
||||
indexer_settings.availability_delay -= 1;
|
||||
}
|
||||
ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput => {
|
||||
if indexer_settings.rss_sync_interval > 0 {
|
||||
ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput
|
||||
if indexer_settings.rss_sync_interval > 0 =>
|
||||
{
|
||||
indexer_settings.rss_sync_interval -= 1;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -272,10 +272,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
|
||||
.whitelisted_hardcoded_subs
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
|
||||
ActiveRadarrBlock::AllIndexerSettingsPrompt
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== ActiveRadarrBlock::IndexerSettingsConfirmPrompt
|
||||
&& matches_key!(confirm, self.key)
|
||||
&& matches_key!(confirm, self.key) =>
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(
|
||||
@@ -285,7 +285,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,17 +539,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::AddMoviePrompt => {
|
||||
ActiveRadarrBlock::AddMoviePrompt
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== ActiveRadarrBlock::AddMovieConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
&& matches_key!(confirm, key) =>
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action =
|
||||
Some(RadarrEvent::AddMovie(self.build_add_movie_body()));
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,10 +376,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::EditMoviePrompt => {
|
||||
ActiveRadarrBlock::EditMoviePrompt
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== ActiveRadarrBlock::EditMovieConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
&& matches_key!(confirm, key) =>
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action =
|
||||
@@ -388,7 +388,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,10 +506,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
|
||||
.tags
|
||||
);
|
||||
}
|
||||
ActiveSonarrBlock::EditIndexerPrompt => {
|
||||
ActiveSonarrBlock::EditIndexerPrompt
|
||||
if self.app.data.sonarr_data.selected_block.get_active_block()
|
||||
== ActiveSonarrBlock::EditIndexerConfirmPrompt
|
||||
&& matches_key!(confirm, self.key)
|
||||
&& matches_key!(confirm, self.key) =>
|
||||
{
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
@@ -518,7 +518,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +106,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
|
||||
indexer_settings.maximum_size -= 1;
|
||||
}
|
||||
}
|
||||
ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput => {
|
||||
if indexer_settings.rss_sync_interval > 0 {
|
||||
ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput
|
||||
if indexer_settings.rss_sync_interval > 0 =>
|
||||
{
|
||||
indexer_settings.rss_sync_interval -= 1;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,17 +606,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesPrompt => {
|
||||
ActiveSonarrBlock::AddSeriesPrompt
|
||||
if self.app.data.sonarr_data.selected_block.get_active_block()
|
||||
== ActiveSonarrBlock::AddSeriesConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
&& matches_key!(confirm, key) =>
|
||||
{
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::AddSeries(self.build_add_series_body()));
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,10 +450,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveSonarrBlock::EditSeriesPrompt => {
|
||||
ActiveSonarrBlock::EditSeriesPrompt
|
||||
if self.app.data.sonarr_data.selected_block.get_active_block()
|
||||
== ActiveSonarrBlock::EditSeriesConfirmPrompt
|
||||
&& matches_key!(confirm, key)
|
||||
&& matches_key!(confirm, key) =>
|
||||
{
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
@@ -462,7 +462,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,8 +279,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler
|
||||
}
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt => {
|
||||
if self.app.data.sonarr_data.prompt_confirm {
|
||||
let (series_id, season_number) = self.extract_series_id_season_number_tuple();
|
||||
self.app.data.sonarr_data.prompt_confirm_action = Some(
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(self.extract_series_id_season_number_tuple()),
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(series_id, season_number),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -404,8 +405,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler
|
||||
},
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt if matches_key!(confirm, key) => {
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
let (series_id, season_number) = self.extract_series_id_season_number_tuple();
|
||||
self.app.data.sonarr_data.prompt_confirm_action = Some(
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(self.extract_series_id_season_number_tuple()),
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(series_id, season_number),
|
||||
);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
|
||||
@@ -268,7 +268,7 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch((0, 0))
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(0, 0)
|
||||
)]
|
||||
#[case(
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
||||
@@ -694,7 +694,7 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch((0, 0))
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(0, 0)
|
||||
)]
|
||||
#[case(
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
||||
|
||||
@@ -278,8 +278,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler
|
||||
}
|
||||
_ if matches_key!(toggle_monitoring, key) => {
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
let (series_id, season_number) = self.extract_series_id_season_number_tuple();
|
||||
self.app.data.sonarr_data.prompt_confirm_action = Some(
|
||||
SonarrEvent::ToggleSeasonMonitoring(self.extract_series_id_season_number_tuple()),
|
||||
SonarrEvent::ToggleSeasonMonitoring(series_id, season_number),
|
||||
);
|
||||
|
||||
self
|
||||
|
||||
@@ -378,7 +378,7 @@ mod tests {
|
||||
assert!(app.is_routing);
|
||||
assert_some_eq_x!(
|
||||
&app.data.sonarr_data.prompt_confirm_action,
|
||||
&SonarrEvent::ToggleSeasonMonitoring((0, 0))
|
||||
&SonarrEvent::ToggleSeasonMonitoring(0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -893,7 +893,7 @@ mod tests {
|
||||
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
|
||||
|
||||
let mut expected_vec = movies_vec();
|
||||
expected_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
expected_vec.sort_by_key(|a| a.id);
|
||||
expected_vec.reverse();
|
||||
|
||||
TableHandlerUnit::new(
|
||||
|
||||
@@ -36,8 +36,6 @@ pub const READARR_LOGO: &str = "⠀⠀⠀⠀⠀⣀⣠⣤⣄⣀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠈⠳⣬⣙⠻⠿⠟⣋⣥⠞⠁⠀⠀
|
||||
⠀⠀⠀⠀⠀⠉⠙⠛⠋⠉⠀⠀⠀⠀⠀
|
||||
";
|
||||
// Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then
|
||||
#[allow(dead_code)]
|
||||
pub const LIDARR_LOGO: &str = "⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀
|
||||
⠀⢠⣾⠟⠋⠀⠀⢀⣀⠀⠙⠻⣷⡄⠀
|
||||
⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄
|
||||
|
||||
+26
-6
@@ -2,7 +2,7 @@
|
||||
#[macro_use]
|
||||
extern crate assertables;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{
|
||||
Args, CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version,
|
||||
};
|
||||
@@ -86,7 +86,7 @@ struct GlobalOpts {
|
||||
global = true,
|
||||
value_parser,
|
||||
env = "MANAGARR_CONFIG_FILE",
|
||||
help = "The Managarr configuration file to use"
|
||||
help = "The Managarr configuration file to use; defaults to the path shown by 'managarr config-path'"
|
||||
)]
|
||||
config_file: Option<PathBuf>,
|
||||
#[arg(
|
||||
@@ -127,15 +127,30 @@ async fn main() -> Result<()> {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
let args = Cli::parse();
|
||||
let mut config = if let Some(ref config_file) = args.global.config_file {
|
||||
load_config(config_file.to_str().expect("Invalid config file specified"))?
|
||||
let config_file_path = confy::get_configuration_file_path("managarr", "config")?;
|
||||
let default_config_path = config_file_path.display().to_string();
|
||||
|
||||
if matches!(args.command, Some(Command::ConfigPath)) {
|
||||
println!("{default_config_path}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (mut config, config_path) = if let Some(ref config_file) = args.global.config_file {
|
||||
(
|
||||
load_config(config_file.to_str().expect("Invalid config file specified"))?,
|
||||
config_file.display().to_string(),
|
||||
)
|
||||
} else {
|
||||
confy::load("managarr", "config")?
|
||||
(
|
||||
confy::load("managarr", "config")
|
||||
.with_context(|| format!("Config file at '{default_config_path}' is invalid"))?,
|
||||
default_config_path,
|
||||
)
|
||||
};
|
||||
let theme_name = config.theme.clone();
|
||||
let spinner_disabled = args.global.disable_spinner;
|
||||
debug!("Managarr loaded using config: {config:?}");
|
||||
config.validate();
|
||||
config.validate(&config_path);
|
||||
config.post_process_initialization();
|
||||
|
||||
let reqwest_client = build_network_client(&config);
|
||||
@@ -170,6 +185,11 @@ async fn main() -> Result<()> {
|
||||
generate(shell, &mut cli, "managarr", &mut io::stdout())
|
||||
}
|
||||
Command::TailLogs { no_color } => tail_logs(no_color).await?,
|
||||
Command::ConfigPath => {
|
||||
unreachable!(
|
||||
"ConfigPath command is handled before this match and should be unreachable here"
|
||||
);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let app_nw = Arc::clone(&app);
|
||||
|
||||
@@ -422,6 +422,8 @@ pub struct LidarrHistoryItem {
|
||||
pub album_id: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub artist_id: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub track_id: i64,
|
||||
#[serde(default)]
|
||||
pub quality: QualityWrapper,
|
||||
pub date: DateTime<Utc>,
|
||||
@@ -497,6 +499,28 @@ pub struct LidarrReleaseDownloadBody {
|
||||
pub indexer_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlocklistItem {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub artist_id: i64,
|
||||
pub album_ids: Option<Vec<Number>>,
|
||||
pub source_title: String,
|
||||
pub quality: QualityWrapper,
|
||||
pub date: DateTime<Utc>,
|
||||
pub protocol: String,
|
||||
pub indexer: String,
|
||||
pub message: String,
|
||||
pub artist: Artist,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BlocklistResponse {
|
||||
pub records: Vec<BlocklistItem>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TrackFile {
|
||||
@@ -556,6 +580,7 @@ pub struct Track {
|
||||
pub duration: i64,
|
||||
pub has_file: bool,
|
||||
pub ratings: Ratings,
|
||||
pub track_file: Option<TrackFile>,
|
||||
}
|
||||
|
||||
impl From<LidarrSerdeable> for Serdeable {
|
||||
@@ -571,6 +596,7 @@ serde_enum_from!(
|
||||
Album(Album),
|
||||
Artist(Artist),
|
||||
Artists(Vec<Artist>),
|
||||
BlocklistResponse(BlocklistResponse),
|
||||
DiskSpaces(Vec<DiskSpace>),
|
||||
DownloadsResponse(DownloadsResponse),
|
||||
LidarrHistoryWrapper(LidarrHistoryWrapper),
|
||||
|
||||
@@ -5,10 +5,10 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::models::lidarr_models::{
|
||||
AddArtistSearchResult, Album, AudioTags, DownloadRecord, DownloadStatus, DownloadsResponse,
|
||||
LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, LidarrRelease, LidarrTask,
|
||||
MediaInfo, Member, MetadataProfile, MonitorType, NewItemMonitorType, SystemStatus, Track,
|
||||
TrackFile,
|
||||
AddArtistSearchResult, Album, AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord,
|
||||
DownloadStatus, DownloadsResponse, LidarrHistoryEventType, LidarrHistoryItem,
|
||||
LidarrHistoryWrapper, LidarrRelease, LidarrTask, MediaInfo, Member, MetadataProfile,
|
||||
MonitorType, NewItemMonitorType, SystemStatus, Track, TrackFile,
|
||||
};
|
||||
use crate::models::servarr_models::{
|
||||
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
|
||||
@@ -276,9 +276,27 @@ mod tests {
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Artist(artist));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_blocklist_response() {
|
||||
let blocklist_response = BlocklistResponse {
|
||||
records: vec![BlocklistItem {
|
||||
id: 1,
|
||||
..BlocklistItem::default()
|
||||
}],
|
||||
};
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = blocklist_response.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
lidarr_serdeable,
|
||||
LidarrSerdeable::BlocklistResponse(blocklist_response)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_disk_spaces() {
|
||||
let disk_spaces = vec![DiskSpace {
|
||||
path: Some("/path".to_owned()),
|
||||
free_space: 1,
|
||||
total_space: 1,
|
||||
}];
|
||||
|
||||
@@ -233,6 +233,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_disk_spaces() {
|
||||
let disk_spaces = vec![DiskSpace {
|
||||
path: Some("/path".to_owned()),
|
||||
free_space: 1,
|
||||
total_space: 1,
|
||||
}];
|
||||
|
||||
@@ -2,14 +2,14 @@ use serde_json::Number;
|
||||
|
||||
use super::modals::{AddArtistModal, AddRootFolderModal, AlbumDetailsModal, EditArtistModal};
|
||||
use crate::app::context_clues::{
|
||||
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
|
||||
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::lidarr_models::{LidarrRelease, LidarrTask};
|
||||
use crate::models::lidarr_models::{BlocklistItem, LidarrRelease, LidarrTask};
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_models::{IndexerSettings, QueueEvent};
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
@@ -27,8 +27,10 @@ use itertools::Itertools;
|
||||
use strum::EnumIter;
|
||||
#[cfg(test)]
|
||||
use {
|
||||
super::modals::TrackDetailsModal,
|
||||
crate::models::lidarr_models::{MonitorType, NewItemMonitorType},
|
||||
crate::models::stateful_table::SortOption,
|
||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::blocklist_item,
|
||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings,
|
||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map,
|
||||
crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
@@ -63,6 +65,7 @@ pub struct LidarrData<'a> {
|
||||
pub artist_history: StatefulTable<LidarrHistoryItem>,
|
||||
pub artist_info_tabs: TabState,
|
||||
pub artists: StatefulTable<Artist>,
|
||||
pub blocklist: StatefulTable<BlocklistItem>,
|
||||
pub delete_files: bool,
|
||||
pub discography_releases: StatefulTable<LidarrRelease>,
|
||||
pub disk_space_vec: Vec<DiskSpace>,
|
||||
@@ -148,6 +151,7 @@ impl<'a> Default for LidarrData<'a> {
|
||||
album_details_modal: None,
|
||||
artist_history: StatefulTable::default(),
|
||||
artists: StatefulTable::default(),
|
||||
blocklist: StatefulTable::default(),
|
||||
delete_files: false,
|
||||
discography_releases: StatefulTable::default(),
|
||||
disk_space_vec: Vec::new(),
|
||||
@@ -186,6 +190,12 @@ impl<'a> Default for LidarrData<'a> {
|
||||
contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES),
|
||||
config: None,
|
||||
},
|
||||
TabRoute {
|
||||
title: "Blocklist".to_string(),
|
||||
route: ActiveLidarrBlock::Blocklist.into(),
|
||||
contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES),
|
||||
config: None,
|
||||
},
|
||||
TabRoute {
|
||||
title: "History".to_string(),
|
||||
route: ActiveLidarrBlock::History.into(),
|
||||
@@ -292,7 +302,23 @@ impl LidarrData<'_> {
|
||||
.metadata_profile_list
|
||||
.set_items(vec![metadata_profile().name]);
|
||||
|
||||
let mut album_details_modal = AlbumDetailsModal::default();
|
||||
let mut track_details_modal = TrackDetailsModal {
|
||||
track_details: ScrollableText::with_string("Some details".to_owned()),
|
||||
..TrackDetailsModal::default()
|
||||
};
|
||||
track_details_modal
|
||||
.track_history
|
||||
.set_items(vec![lidarr_history_item()]);
|
||||
track_details_modal.track_history.search = Some("track history search".into());
|
||||
track_details_modal.track_history.filter = Some("track history filter".into());
|
||||
track_details_modal
|
||||
.track_history
|
||||
.sorting(vec![sort_option!(id)]);
|
||||
|
||||
let mut album_details_modal = AlbumDetailsModal {
|
||||
track_details_modal: Some(track_details_modal),
|
||||
..AlbumDetailsModal::default()
|
||||
};
|
||||
album_details_modal.tracks.set_items(vec![track()]);
|
||||
album_details_modal.tracks.search = Some("album search".into());
|
||||
album_details_modal
|
||||
@@ -362,6 +388,8 @@ impl LidarrData<'_> {
|
||||
}]);
|
||||
lidarr_data.artists.search = Some("artist search".into());
|
||||
lidarr_data.artists.filter = Some("artist filter".into());
|
||||
lidarr_data.blocklist.set_items(vec![blocklist_item()]);
|
||||
lidarr_data.blocklist.sorting(vec![sort_option!(id)]);
|
||||
lidarr_data.downloads.set_items(vec![download_record()]);
|
||||
lidarr_data.history.set_items(vec![lidarr_history_item()]);
|
||||
lidarr_data.history.sorting(vec![SortOption {
|
||||
@@ -429,6 +457,11 @@ pub enum ActiveLidarrBlock {
|
||||
AllIndexerSettingsPrompt,
|
||||
AutomaticallySearchAlbumPrompt,
|
||||
AutomaticallySearchArtistPrompt,
|
||||
Blocklist,
|
||||
BlocklistItemDetails,
|
||||
DeleteBlocklistItemPrompt,
|
||||
BlocklistClearAllItemsPrompt,
|
||||
BlocklistSortPrompt,
|
||||
DeleteAlbumPrompt,
|
||||
DeleteAlbumConfirmPrompt,
|
||||
DeleteAlbumToggleDeleteFile,
|
||||
@@ -469,6 +502,8 @@ pub enum ActiveLidarrBlock {
|
||||
FilterHistoryError,
|
||||
FilterArtistHistory,
|
||||
FilterArtistHistoryError,
|
||||
FilterTrackHistory,
|
||||
FilterTrackHistoryError,
|
||||
History,
|
||||
HistoryItemDetails,
|
||||
HistorySortPrompt,
|
||||
@@ -499,12 +534,18 @@ pub enum ActiveLidarrBlock {
|
||||
SearchArtistHistoryError,
|
||||
SearchTracks,
|
||||
SearchTracksError,
|
||||
SearchTrackHistory,
|
||||
SearchTrackHistoryError,
|
||||
System,
|
||||
SystemLogs,
|
||||
SystemQueuedEvents,
|
||||
SystemTasks,
|
||||
SystemTaskStartConfirmPrompt,
|
||||
SystemUpdates,
|
||||
TrackDetails,
|
||||
TrackHistory,
|
||||
TrackHistoryDetails,
|
||||
TrackHistorySortPrompt,
|
||||
UpdateAllArtistsPrompt,
|
||||
UpdateAndScanArtistPrompt,
|
||||
UpdateDownloadsPrompt,
|
||||
@@ -556,6 +597,14 @@ pub static ALBUM_DETAILS_BLOCKS: [ActiveLidarrBlock; 15] = [
|
||||
ActiveLidarrBlock::DeleteTrackFilePrompt,
|
||||
];
|
||||
|
||||
pub static BLOCKLIST_BLOCKS: [ActiveLidarrBlock; 5] = [
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::BlocklistItemDetails,
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt,
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt,
|
||||
ActiveLidarrBlock::BlocklistSortPrompt,
|
||||
];
|
||||
|
||||
pub static DOWNLOADS_BLOCKS: [ActiveLidarrBlock; 3] = [
|
||||
ActiveLidarrBlock::Downloads,
|
||||
ActiveLidarrBlock::DeleteDownloadPrompt,
|
||||
@@ -767,6 +816,17 @@ pub static SYSTEM_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
|
||||
ActiveLidarrBlock::SystemUpdates,
|
||||
];
|
||||
|
||||
pub static TRACK_DETAILS_BLOCKS: [ActiveLidarrBlock; 8] = [
|
||||
ActiveLidarrBlock::TrackDetails,
|
||||
ActiveLidarrBlock::TrackHistory,
|
||||
ActiveLidarrBlock::TrackHistoryDetails,
|
||||
ActiveLidarrBlock::SearchTrackHistory,
|
||||
ActiveLidarrBlock::SearchTrackHistoryError,
|
||||
ActiveLidarrBlock::FilterTrackHistory,
|
||||
ActiveLidarrBlock::FilterTrackHistoryError,
|
||||
ActiveLidarrBlock::TrackHistorySortPrompt,
|
||||
];
|
||||
|
||||
impl From<ActiveLidarrBlock> for Route {
|
||||
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
|
||||
Route::Lidarr(active_lidarr_block, None)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::context_clues::{
|
||||
DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
|
||||
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
|
||||
@@ -11,11 +11,12 @@ mod tests {
|
||||
use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS,
|
||||
ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS,
|
||||
ARTIST_DETAILS_BLOCKS, BLOCKLIST_BLOCKS, DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS,
|
||||
DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS,
|
||||
EDIT_ARTIST_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
|
||||
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
||||
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
|
||||
TRACK_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::{
|
||||
BlockSelectionState, Route,
|
||||
@@ -148,6 +149,7 @@ mod tests {
|
||||
assert_none!(lidarr_data.album_details_modal);
|
||||
assert_is_empty!(lidarr_data.artists);
|
||||
assert_is_empty!(lidarr_data.artist_history);
|
||||
assert_is_empty!(lidarr_data.blocklist);
|
||||
assert!(!lidarr_data.delete_files);
|
||||
assert_is_empty!(lidarr_data.disk_space_vec);
|
||||
assert_is_empty!(lidarr_data.downloads);
|
||||
@@ -170,7 +172,7 @@ mod tests {
|
||||
assert_is_empty!(lidarr_data.updates);
|
||||
assert_is_empty!(lidarr_data.version);
|
||||
|
||||
assert_eq!(lidarr_data.main_tabs.tabs.len(), 6);
|
||||
assert_eq!(lidarr_data.main_tabs.tabs.len(), 7);
|
||||
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library");
|
||||
assert_eq!(
|
||||
@@ -194,50 +196,61 @@ mod tests {
|
||||
);
|
||||
assert_none!(lidarr_data.main_tabs.tabs[1].config);
|
||||
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[2].title, "History");
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[2].title, "Blocklist");
|
||||
assert_eq!(
|
||||
lidarr_data.main_tabs.tabs[2].route,
|
||||
ActiveLidarrBlock::History.into()
|
||||
ActiveLidarrBlock::Blocklist.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&lidarr_data.main_tabs.tabs[2].contextual_help,
|
||||
&HISTORY_CONTEXT_CLUES
|
||||
&BLOCKLIST_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.main_tabs.tabs[2].config);
|
||||
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[3].title, "Root Folders");
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[3].title, "History");
|
||||
assert_eq!(
|
||||
lidarr_data.main_tabs.tabs[3].route,
|
||||
ActiveLidarrBlock::RootFolders.into()
|
||||
ActiveLidarrBlock::History.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&lidarr_data.main_tabs.tabs[3].contextual_help,
|
||||
&ROOT_FOLDERS_CONTEXT_CLUES
|
||||
&HISTORY_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.main_tabs.tabs[3].config);
|
||||
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[4].title, "Indexers");
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[4].title, "Root Folders");
|
||||
assert_eq!(
|
||||
lidarr_data.main_tabs.tabs[4].route,
|
||||
ActiveLidarrBlock::Indexers.into()
|
||||
ActiveLidarrBlock::RootFolders.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&lidarr_data.main_tabs.tabs[4].contextual_help,
|
||||
&INDEXERS_CONTEXT_CLUES
|
||||
&ROOT_FOLDERS_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.main_tabs.tabs[4].config);
|
||||
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[5].title, "System");
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[5].title, "Indexers");
|
||||
assert_eq!(
|
||||
lidarr_data.main_tabs.tabs[5].route,
|
||||
ActiveLidarrBlock::System.into()
|
||||
ActiveLidarrBlock::Indexers.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&lidarr_data.main_tabs.tabs[5].contextual_help,
|
||||
&SYSTEM_CONTEXT_CLUES
|
||||
&INDEXERS_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.main_tabs.tabs[5].config);
|
||||
|
||||
assert_str_eq!(lidarr_data.main_tabs.tabs[6].title, "System");
|
||||
assert_eq!(
|
||||
lidarr_data.main_tabs.tabs[6].route,
|
||||
ActiveLidarrBlock::System.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&lidarr_data.main_tabs.tabs[6].contextual_help,
|
||||
&SYSTEM_CONTEXT_CLUES
|
||||
);
|
||||
assert_none!(lidarr_data.main_tabs.tabs[6].config);
|
||||
|
||||
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 3);
|
||||
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
|
||||
assert_eq!(
|
||||
@@ -325,6 +338,16 @@ mod tests {
|
||||
assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::DeleteTrackFilePrompt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_blocks_contents() {
|
||||
assert_eq!(BLOCKLIST_BLOCKS.len(), 5);
|
||||
assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::Blocklist));
|
||||
assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::BlocklistItemDetails));
|
||||
assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteBlocklistItemPrompt));
|
||||
assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::BlocklistClearAllItemsPrompt));
|
||||
assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::BlocklistSortPrompt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_downloads_blocks_contains_expected_blocks() {
|
||||
assert_eq!(DOWNLOADS_BLOCKS.len(), 3);
|
||||
@@ -695,4 +718,17 @@ mod tests {
|
||||
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTaskStartConfirmPrompt));
|
||||
assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemUpdates));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_blocks_contents() {
|
||||
assert_eq!(TRACK_DETAILS_BLOCKS.len(), 8);
|
||||
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackDetails));
|
||||
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistory));
|
||||
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistoryDetails));
|
||||
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTrackHistory));
|
||||
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTrackHistoryError));
|
||||
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterTrackHistory));
|
||||
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterTrackHistoryError));
|
||||
assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistorySortPrompt));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use super::lidarr_data::{ActiveLidarrBlock, LidarrData};
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
|
||||
TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track, TrackFile};
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_models::Indexer;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{
|
||||
HorizontallyScrollableText, TabRoute, TabState,
|
||||
HorizontallyScrollableText, ScrollableText, TabRoute, TabState,
|
||||
lidarr_models::{MonitorType, NewItemMonitorType},
|
||||
servarr_models::RootFolder,
|
||||
stateful_list::StatefulList,
|
||||
@@ -226,7 +227,7 @@ impl From<&LidarrData<'_>> for AddRootFolderModal {
|
||||
pub struct AlbumDetailsModal {
|
||||
pub tracks: StatefulTable<Track>,
|
||||
pub track_files: StatefulTable<TrackFile>,
|
||||
// pub track_details_modal: Option<EpisodeDetailsModal>,
|
||||
pub track_details_modal: Option<TrackDetailsModal>,
|
||||
pub album_history: StatefulTable<LidarrHistoryItem>,
|
||||
pub album_releases: StatefulTable<LidarrRelease>,
|
||||
pub album_details_tabs: TabState,
|
||||
@@ -236,7 +237,7 @@ impl Default for AlbumDetailsModal {
|
||||
fn default() -> AlbumDetailsModal {
|
||||
AlbumDetailsModal {
|
||||
tracks: StatefulTable::default(),
|
||||
// TODO episode_details_modal: None,
|
||||
track_details_modal: None,
|
||||
track_files: StatefulTable::default(),
|
||||
album_releases: StatefulTable::default(),
|
||||
album_history: StatefulTable::default(),
|
||||
@@ -263,3 +264,33 @@ impl Default for AlbumDetailsModal {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub struct TrackDetailsModal {
|
||||
pub track_details: ScrollableText,
|
||||
pub track_history: StatefulTable<LidarrHistoryItem>,
|
||||
pub track_details_tabs: TabState,
|
||||
}
|
||||
|
||||
impl Default for TrackDetailsModal {
|
||||
fn default() -> Self {
|
||||
TrackDetailsModal {
|
||||
track_details: ScrollableText::default(),
|
||||
track_history: StatefulTable::default(),
|
||||
track_details_tabs: TabState::new(vec![
|
||||
TabRoute {
|
||||
title: "Track Details".to_string(),
|
||||
route: ActiveLidarrBlock::TrackDetails.into(),
|
||||
contextual_help: Some(&TRACK_DETAILS_CONTEXT_CLUES),
|
||||
config: None,
|
||||
},
|
||||
TabRoute {
|
||||
title: "History".to_string(),
|
||||
route: ActiveLidarrBlock::TrackHistory.into(),
|
||||
contextual_help: Some(&TRACK_HISTORY_CONTEXT_CLUES),
|
||||
config: None,
|
||||
},
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
mod tests {
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
|
||||
TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
|
||||
use crate::models::servarr_data::lidarr::modals::{
|
||||
AddArtistModal, AlbumDetailsModal, EditArtistModal,
|
||||
AddArtistModal, AlbumDetailsModal, EditArtistModal, TrackDetailsModal,
|
||||
};
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
|
||||
@@ -92,14 +93,14 @@ mod tests {
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
path: "/nfs/music/test_artist".to_owned(),
|
||||
tags: vec![serde_json::Number::from(1)],
|
||||
tags: vec![Number::from(1)],
|
||||
..Artist::default()
|
||||
};
|
||||
lidarr_data.artists.set_items(vec![artist]);
|
||||
|
||||
let edit_artist_modal = EditArtistModal::from(&lidarr_data);
|
||||
|
||||
assert_eq!(edit_artist_modal.monitored, Some(true));
|
||||
assert_some_eq_x!(&edit_artist_modal.monitored, &true);
|
||||
assert_eq!(
|
||||
*edit_artist_modal.monitor_list.current_selection(),
|
||||
NewItemMonitorType::All
|
||||
@@ -205,24 +206,24 @@ mod tests {
|
||||
let edit_indexer_modal = EditIndexerModal::from(&lidarr_data);
|
||||
|
||||
assert_str_eq!(edit_indexer_modal.name.text, "Test");
|
||||
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
|
||||
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
|
||||
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
|
||||
assert_some_eq_x!(&edit_indexer_modal.enable_rss, &true);
|
||||
assert_some_eq_x!(&edit_indexer_modal.enable_automatic_search, &true);
|
||||
assert_some_eq_x!(&edit_indexer_modal.enable_interactive_search, &true);
|
||||
assert_eq!(edit_indexer_modal.priority, 1);
|
||||
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
|
||||
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
|
||||
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
|
||||
assert_is_empty!(edit_indexer_modal.seed_ratio.text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_details_modal_default() {
|
||||
let album_details_modal = AlbumDetailsModal::default();
|
||||
|
||||
assert!(album_details_modal.tracks.is_empty());
|
||||
// assert!(album_details_modal.track_details_modal.is_none());
|
||||
assert!(album_details_modal.track_files.is_empty());
|
||||
assert!(album_details_modal.album_releases.is_empty());
|
||||
assert!(album_details_modal.album_history.is_empty());
|
||||
assert_is_empty!(album_details_modal.tracks);
|
||||
assert_none!(album_details_modal.track_details_modal);
|
||||
assert_is_empty!(album_details_modal.track_files);
|
||||
assert_is_empty!(album_details_modal.album_releases);
|
||||
assert_is_empty!(album_details_modal.album_history);
|
||||
|
||||
assert_eq!(album_details_modal.album_details_tabs.tabs.len(), 3);
|
||||
|
||||
@@ -234,15 +235,8 @@ mod tests {
|
||||
album_details_modal.album_details_tabs.tabs[0].route,
|
||||
ActiveLidarrBlock::AlbumDetails.into()
|
||||
);
|
||||
assert!(
|
||||
album_details_modal.album_details_tabs.tabs[0]
|
||||
.contextual_help
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
album_details_modal.album_details_tabs.tabs[0]
|
||||
.contextual_help
|
||||
.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&album_details_modal.album_details_tabs.tabs[0].contextual_help,
|
||||
&ALBUM_DETAILS_CONTEXT_CLUES
|
||||
);
|
||||
assert_eq!(album_details_modal.album_details_tabs.tabs[0].config, None);
|
||||
@@ -255,15 +249,8 @@ mod tests {
|
||||
album_details_modal.album_details_tabs.tabs[1].route,
|
||||
ActiveLidarrBlock::AlbumHistory.into()
|
||||
);
|
||||
assert!(
|
||||
album_details_modal.album_details_tabs.tabs[1]
|
||||
.contextual_help
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
album_details_modal.album_details_tabs.tabs[1]
|
||||
.contextual_help
|
||||
.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&album_details_modal.album_details_tabs.tabs[1].contextual_help,
|
||||
&ALBUM_HISTORY_CONTEXT_CLUES
|
||||
);
|
||||
assert_eq!(album_details_modal.album_details_tabs.tabs[1].config, None);
|
||||
@@ -276,17 +263,48 @@ mod tests {
|
||||
album_details_modal.album_details_tabs.tabs[2].route,
|
||||
ActiveLidarrBlock::ManualAlbumSearch.into()
|
||||
);
|
||||
assert!(
|
||||
album_details_modal.album_details_tabs.tabs[2]
|
||||
.contextual_help
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
album_details_modal.album_details_tabs.tabs[2]
|
||||
.contextual_help
|
||||
.unwrap(),
|
||||
assert_some_eq_x!(
|
||||
&album_details_modal.album_details_tabs.tabs[2].contextual_help,
|
||||
&MANUAL_ALBUM_SEARCH_CONTEXT_CLUES
|
||||
);
|
||||
assert_eq!(album_details_modal.album_details_tabs.tabs[2].config, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_modal_default() {
|
||||
let track_details_modal = TrackDetailsModal::default();
|
||||
|
||||
assert_is_empty!(track_details_modal.track_details);
|
||||
assert_is_empty!(track_details_modal.track_history);
|
||||
|
||||
assert_eq!(track_details_modal.track_details_tabs.tabs.len(), 2);
|
||||
|
||||
assert_str_eq!(
|
||||
track_details_modal.track_details_tabs.tabs[0].title,
|
||||
"Track Details"
|
||||
);
|
||||
assert_eq!(
|
||||
track_details_modal.track_details_tabs.tabs[0].route,
|
||||
ActiveLidarrBlock::TrackDetails.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&track_details_modal.track_details_tabs.tabs[0].contextual_help,
|
||||
&TRACK_DETAILS_CONTEXT_CLUES
|
||||
);
|
||||
assert_eq!(track_details_modal.track_details_tabs.tabs[0].config, None);
|
||||
|
||||
assert_str_eq!(
|
||||
track_details_modal.track_details_tabs.tabs[1].title,
|
||||
"History"
|
||||
);
|
||||
assert_eq!(
|
||||
track_details_modal.track_details_tabs.tabs[1].route,
|
||||
ActiveLidarrBlock::TrackHistory.into()
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&track_details_modal.track_details_tabs.tabs[1].contextual_help,
|
||||
&TRACK_HISTORY_CONTEXT_CLUES
|
||||
);
|
||||
assert_eq!(track_details_modal.track_details_tabs.tabs[1].config, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,23 @@ pub(in crate::models::servarr_data) mod data_test_utils;
|
||||
#[cfg(test)]
|
||||
mod servarr_data_tests;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Notification {
|
||||
pub title: String,
|
||||
pub message: String,
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub fn new(title: String, message: String, success: bool) -> Self {
|
||||
Self {
|
||||
title,
|
||||
message,
|
||||
success,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
pub enum ActiveKeybindingBlock {
|
||||
#[default]
|
||||
|
||||
@@ -83,6 +83,7 @@ pub struct CommandBody {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DiskSpace {
|
||||
pub path: Option<String>,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub free_space: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
|
||||
@@ -427,6 +427,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_sonarr_serdeable_from_disk_spaces() {
|
||||
let disk_spaces = vec![DiskSpace {
|
||||
path: Some("/path".to_owned()),
|
||||
free_space: 1,
|
||||
total_space: 1,
|
||||
}];
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::{Artist, BlocklistItem, BlocklistResponse, LidarrSerdeable};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
artist, blocklist_item,
|
||||
};
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::{Number, json};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_clear_lidarr_blocklist_event() {
|
||||
let blocklist_items = vec![
|
||||
BlocklistItem {
|
||||
id: 1,
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 2,
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 3,
|
||||
..blocklist_item()
|
||||
},
|
||||
];
|
||||
let expected_request_json = json!({ "ids": [1, 2, 3]});
|
||||
let (mock, app, _server) = MockServarrApi::delete()
|
||||
.with_request_body(expected_request_json)
|
||||
.build_for(LidarrEvent::ClearBlocklist)
|
||||
.await;
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.blocklist
|
||||
.set_items(blocklist_items);
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
assert!(
|
||||
network
|
||||
.handle_lidarr_event(LidarrEvent::ClearBlocklist)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_lidarr_blocklist_item_event() {
|
||||
let (mock, app, _server) = MockServarrApi::delete()
|
||||
.path("/1")
|
||||
.build_for(LidarrEvent::DeleteBlocklistItem(1))
|
||||
.await;
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.blocklist
|
||||
.set_items(vec![blocklist_item()]);
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
assert!(
|
||||
network
|
||||
.handle_lidarr_event(LidarrEvent::DeleteBlocklistItem(1))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_lidarr_blocklist_event(#[values(true, false)] use_custom_sorting: bool) {
|
||||
let blocklist_json = json!({"records": [{
|
||||
"artistId": 1007,
|
||||
"albumIds": [42020],
|
||||
"sourceTitle": "z artist",
|
||||
"quality": { "quality": { "name": "Lossless" }},
|
||||
"date": "2023-05-20T21:29:16Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "NZBgeek (Prowlarr)",
|
||||
"message": "test message",
|
||||
"id": 123,
|
||||
"artist": {
|
||||
"id": 1,
|
||||
"artistName": "Alex",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"overview": "some interesting description of the artist",
|
||||
"artistType": "Person",
|
||||
"disambiguation": "American pianist",
|
||||
"path": "/nfs/music/test-artist",
|
||||
"members": [{"name": "alex", "instrument": "piano"}],
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"monitorNewItems": "all",
|
||||
"genres": ["soundtrack"],
|
||||
"tags": [1],
|
||||
"added": "2023-01-01T00:00:00Z",
|
||||
"ratings": { "votes": 15, "value": 8.4 },
|
||||
"statistics": {
|
||||
"albumCount": 1,
|
||||
"trackFileCount": 15,
|
||||
"trackCount": 15,
|
||||
"totalTrackCount": 15,
|
||||
"sizeOnDisk": 12345,
|
||||
"percentOfTracks": 99.9
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"artistId": 2001,
|
||||
"artistTitle": "Test Artist",
|
||||
"albumIds": [42018],
|
||||
"sourceTitle": "A Artist",
|
||||
"quality": { "quality": { "name": "Lossless" }},
|
||||
"date": "2023-05-20T21:29:16Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "NZBgeek (Prowlarr)",
|
||||
"message": "test message",
|
||||
"id": 456,
|
||||
"artist": {
|
||||
"id": 1,
|
||||
"artistName": "Alex",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"overview": "some interesting description of the artist",
|
||||
"artistType": "Person",
|
||||
"disambiguation": "American pianist",
|
||||
"path": "/nfs/music/test-artist",
|
||||
"members": [{"name": "alex", "instrument": "piano"}],
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"monitorNewItems": "all",
|
||||
"genres": ["soundtrack"],
|
||||
"tags": [1],
|
||||
"added": "2023-01-01T00:00:00Z",
|
||||
"ratings": { "votes": 15, "value": 8.4 },
|
||||
"statistics": {
|
||||
"albumCount": 1,
|
||||
"trackFileCount": 15,
|
||||
"trackCount": 15,
|
||||
"totalTrackCount": 15,
|
||||
"sizeOnDisk": 12345,
|
||||
"percentOfTracks": 99.9
|
||||
}
|
||||
}
|
||||
}]});
|
||||
let response: BlocklistResponse = serde_json::from_value(blocklist_json.clone()).unwrap();
|
||||
let mut expected_blocklist = vec![
|
||||
BlocklistItem {
|
||||
id: 123,
|
||||
artist_id: 1007,
|
||||
source_title: "z artist".into(),
|
||||
album_ids: Some(vec![Number::from(42020)]),
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 456,
|
||||
artist_id: 2001,
|
||||
source_title: "A Artist".into(),
|
||||
album_ids: Some(vec![Number::from(42018)]),
|
||||
..blocklist_item()
|
||||
},
|
||||
];
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(blocklist_json)
|
||||
.build_for(LidarrEvent::GetBlocklist)
|
||||
.await;
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist {
|
||||
id: 1007,
|
||||
artist_name: "Z Artist".into(),
|
||||
..artist()
|
||||
}]);
|
||||
app.lock().await.data.lidarr_data.blocklist.sort_asc = true;
|
||||
if use_custom_sorting {
|
||||
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
};
|
||||
expected_blocklist.sort_by(cmp_fn);
|
||||
|
||||
let blocklist_sort_option = SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.blocklist
|
||||
.sorting(vec![blocklist_sort_option]);
|
||||
}
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let LidarrSerdeable::BlocklistResponse(blocklist) = network
|
||||
.handle_lidarr_event(LidarrEvent::GetBlocklist)
|
||||
.await
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("Expected BlocklistResponse")
|
||||
};
|
||||
mock.assert_async().await;
|
||||
assert_eq!(
|
||||
app.lock().await.data.lidarr_data.blocklist.items,
|
||||
expected_blocklist
|
||||
);
|
||||
assert!(app.lock().await.data.lidarr_data.blocklist.sort_asc);
|
||||
assert_eq!(blocklist, response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_lidarr_blocklist_event_no_op_when_user_is_selecting_sort_options() {
|
||||
let blocklist_json = json!({"records": [{
|
||||
"artistId": 1007,
|
||||
"albumIds": [42020],
|
||||
"sourceTitle": "z artist",
|
||||
"quality": { "quality": { "name": "Lossless" }},
|
||||
"date": "2023-05-20T21:29:16Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "NZBgeek (Prowlarr)",
|
||||
"message": "test message",
|
||||
"id": 123,
|
||||
"artist": {
|
||||
"id": 1,
|
||||
"artistName": "Alex",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"overview": "some interesting description of the artist",
|
||||
"artistType": "Person",
|
||||
"disambiguation": "American pianist",
|
||||
"path": "/nfs/music/test-artist",
|
||||
"members": [{"name": "alex", "instrument": "piano"}],
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"monitorNewItems": "all",
|
||||
"genres": ["soundtrack"],
|
||||
"tags": [1],
|
||||
"added": "2023-01-01T00:00:00Z",
|
||||
"ratings": { "votes": 15, "value": 8.4 },
|
||||
"statistics": {
|
||||
"albumCount": 1,
|
||||
"trackFileCount": 15,
|
||||
"trackCount": 15,
|
||||
"totalTrackCount": 15,
|
||||
"sizeOnDisk": 12345,
|
||||
"percentOfTracks": 99.9
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"artistId": 2001,
|
||||
"albumIds": [42018],
|
||||
"sourceTitle": "A Artist",
|
||||
"quality": { "quality": { "name": "Lossless" }},
|
||||
"date": "2023-05-20T21:29:16Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "NZBgeek (Prowlarr)",
|
||||
"message": "test message",
|
||||
"id": 456,
|
||||
"artist": {
|
||||
"id": 1,
|
||||
"artistName": "Alex",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"overview": "some interesting description of the artist",
|
||||
"artistType": "Person",
|
||||
"disambiguation": "American pianist",
|
||||
"path": "/nfs/music/test-artist",
|
||||
"members": [{"name": "alex", "instrument": "piano"}],
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"monitorNewItems": "all",
|
||||
"genres": ["soundtrack"],
|
||||
"tags": [1],
|
||||
"added": "2023-01-01T00:00:00Z",
|
||||
"ratings": { "votes": 15, "value": 8.4 },
|
||||
"statistics": {
|
||||
"albumCount": 1,
|
||||
"trackFileCount": 15,
|
||||
"trackCount": 15,
|
||||
"totalTrackCount": 15,
|
||||
"sizeOnDisk": 12345,
|
||||
"percentOfTracks": 99.9
|
||||
}
|
||||
}
|
||||
}]});
|
||||
let response: BlocklistResponse = serde_json::from_value(blocklist_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(blocklist_json)
|
||||
.build_for(LidarrEvent::GetBlocklist)
|
||||
.await;
|
||||
app.lock().await.data.lidarr_data.blocklist.sort_asc = true;
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.push_navigation_stack(ActiveLidarrBlock::BlocklistSortPrompt.into());
|
||||
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
};
|
||||
let blocklist_sort_option = SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.blocklist
|
||||
.sorting(vec![blocklist_sort_option]);
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let LidarrSerdeable::BlocklistResponse(blocklist) = network
|
||||
.handle_lidarr_event(LidarrEvent::GetBlocklist)
|
||||
.await
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("Expected BlocklistResponse")
|
||||
};
|
||||
mock.assert_async().await;
|
||||
assert_is_empty!(app.lock().await.data.lidarr_data.blocklist);
|
||||
assert!(app.lock().await.data.lidarr_data.blocklist.sort_asc);
|
||||
assert_eq!(blocklist, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::{BlocklistItem, BlocklistResponse};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_blocklist_network_tests.rs"]
|
||||
mod lidarr_blocklist_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn clear_lidarr_blocklist(&mut self) -> Result<()> {
|
||||
info!("Clearing Lidarr blocklist");
|
||||
let event = LidarrEvent::ClearBlocklist;
|
||||
|
||||
let ids = self
|
||||
.app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.blocklist
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| item.id)
|
||||
.collect::<Vec<i64>>();
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
Some(json!({"ids": ids})),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn delete_lidarr_blocklist_item(
|
||||
&mut self,
|
||||
blocklist_item_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = LidarrEvent::DeleteBlocklistItem(blocklist_item_id);
|
||||
info!("Deleting Lidarr blocklist item for item with id: {blocklist_item_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{blocklist_item_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_blocklist(
|
||||
&mut self,
|
||||
) -> Result<BlocklistResponse> {
|
||||
info!("Fetching Lidarr blocklist");
|
||||
let event = LidarrEvent::GetBlocklist;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), BlocklistResponse>(request_props, |blocklist_resp, mut app| {
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Lidarr(ActiveLidarrBlock::BlocklistSortPrompt, _)
|
||||
) {
|
||||
let mut blocklist_vec: Vec<BlocklistItem> = blocklist_resp.records;
|
||||
blocklist_vec.sort_by_key(|a| a.id);
|
||||
app.data.lidarr_data.blocklist.set_items(blocklist_vec);
|
||||
app.data.lidarr_data.blocklist.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod tests {
|
||||
"sourceTitle": "z album",
|
||||
"albumId": 1007,
|
||||
"artistId": 1007,
|
||||
"trackId": 1007,
|
||||
"quality": { "quality": { "name": "Lossless" } },
|
||||
"date": "2023-01-01T00:00:00Z",
|
||||
"eventType": "grabbed",
|
||||
@@ -30,6 +31,7 @@ mod tests {
|
||||
"sourceTitle": "An Album",
|
||||
"albumId": 2001,
|
||||
"artistId": 2001,
|
||||
"trackId": 2001,
|
||||
"quality": { "quality": { "name": "Lossless" } },
|
||||
"date": "2023-01-01T00:00:00Z",
|
||||
"eventType": "grabbed",
|
||||
@@ -49,6 +51,7 @@ mod tests {
|
||||
id: 123,
|
||||
album_id: 1007,
|
||||
artist_id: 1007,
|
||||
track_id: 1007,
|
||||
source_title: "z album".into(),
|
||||
..lidarr_history_item()
|
||||
},
|
||||
@@ -56,6 +59,7 @@ mod tests {
|
||||
id: 456,
|
||||
album_id: 2001,
|
||||
artist_id: 2001,
|
||||
track_id: 2001,
|
||||
source_title: "An Album".into(),
|
||||
..lidarr_history_item()
|
||||
},
|
||||
@@ -113,6 +117,7 @@ mod tests {
|
||||
"sourceTitle": "z album",
|
||||
"albumId": 1007,
|
||||
"artistId": 1007,
|
||||
"trackId": 1007,
|
||||
"quality": { "quality": { "name": "Lossless" } },
|
||||
"date": "2023-01-01T00:00:00Z",
|
||||
"eventType": "grabbed",
|
||||
@@ -126,6 +131,7 @@ mod tests {
|
||||
"sourceTitle": "An Album",
|
||||
"albumId": 2001,
|
||||
"artistId": 2001,
|
||||
"trackId": 2001,
|
||||
"quality": { "quality": { "name": "Lossless" } },
|
||||
"date": "2023-01-01T00:00:00Z",
|
||||
"eventType": "grabbed",
|
||||
|
||||
@@ -31,7 +31,7 @@ impl Network<'_, '_> {
|
||||
Route::Lidarr(ActiveLidarrBlock::HistorySortPrompt, _)
|
||||
) {
|
||||
let mut history_vec = history_response.records;
|
||||
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
history_vec.sort_by_key(|a| a.id);
|
||||
app.data.lidarr_data.history.set_items(history_vec);
|
||||
app.data.lidarr_data.history.apply_sorting_toggle(false);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user