Compare commits

..
5 Commits
1130 changed files with 4698 additions and 143444 deletions

No files matched your search

-18
View File
@@ -1,18 +0,0 @@
### Description
<!-- What problem is this PR solving? -->
### Alternatives Considered (Optional)
<!-- What alternatives did you consider? Why did you choose this approach? -->
### AI assistance (if any)
<!-- Required if AI assistance was used to write or modify code in this PR. -->
<!-- - List tools here -->
### Authorship & Understanding
- [ ] I wrote or heavily modified this code myself. If I didn't, I gave proper attribution to AI assistance above.
- [ ] 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 above
+9 -36
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
@@ -46,7 +46,7 @@ jobs:
toolchain: [stable, beta]
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
@@ -54,7 +54,7 @@ jobs:
components: clippy
- name: Run clippy action
uses: clechasseur/rs-clippy-check@v5
uses: clechasseur/rs-clippy-check@v3
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
@@ -65,7 +65,7 @@ jobs:
name: nightly / doc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
@@ -76,42 +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.95.0 / check
name: 1.89.0 / check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install 1.95.0
- name: Install 1.89.0
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
toolchain: 1.89.0
- name: cargo +1.95.0 check
- name: cargo +1.89.0 check
run: cargo check
dockerfile-msrv:
# The Dockerfile's pinned Rust image is only built at release time, so a
# rust-version bump that misses it fails silently until the next release.
# Catch the drift on every push instead.
name: dockerfile / msrv
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Check Dockerfile Rust image against Cargo.toml rust-version
run: |
set -euo pipefail
msrv=$(sed -nE 's/^rust-version = "([0-9.]+)"$/\1/p' Cargo.toml)
image_tag=$(sed -nE 's/^FROM rust:([0-9.]+) AS builder$/\1/p' Dockerfile)
: "${msrv:?failed to extract rust-version from Cargo.toml}"
: "${image_tag:?failed to extract the rust image tag from the Dockerfile}"
# The rust image tag pins major.minor, so compare on major.minor only
msrv_mm=$(echo "$msrv" | cut -d. -f1,2)
image_mm=$(echo "$image_tag" | cut -d. -f1,2)
if [ "$(printf '%s\n' "$msrv_mm" "$image_mm" | sort -V | head -n1)" != "$msrv_mm" ]; then
echo "Dockerfile builds with rust:$image_tag, but Cargo.toml declares rust-version = \"$msrv\"."
echo "Cargo refuses to build under an older toolchain, so the release Docker publish step will fail."
echo "Update the FROM rust:* line in the Dockerfile to at least $msrv_mm."
exit 1
fi
echo "OK: Dockerfile rust:$image_tag satisfies rust-version $msrv"
+28 -28
View File
@@ -32,19 +32,19 @@ jobs:
- name: Checkout repository
if: env.ACT != 'true'
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
fetch-depth: 0
- name: Checkout repository
if: env.ACT == 'true'
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v4
with:
python-version: '3.10'
@@ -219,13 +219,13 @@ jobs:
git push origin --follow-tags
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
path: artifacts
- name: Upload the changed Cargo files (Act)
if: env.ACT == 'true'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: bumped-cargo-files
path: |
@@ -267,7 +267,7 @@ jobs:
exit 1
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -279,18 +279,18 @@ jobs:
- name: Get bumped Cargo files (Act)
if: env.ACT == 'true'
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
name: bumped-cargo-files
path: ${{ github.workspace }}
- uses: actions/cache@v5
- uses: actions/cache@v3
name: Cache Cargo registry
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
- uses: actions/cache@v5
- uses: actions/cache@v3
if: startsWith(matrix.job.name, 'linux-')
with:
path: ~/.cargo/bin
@@ -375,7 +375,7 @@ jobs:
cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.sha256 artifacts/
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ env.RELEASE_NAME }}
path: artifacts
@@ -393,12 +393,12 @@ jobs:
exit 1
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
@@ -421,7 +421,7 @@ jobs:
- name: Create a GitHub Release
if: env.ACT != 'true'
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -457,7 +457,7 @@ jobs:
prerelease: false
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
path: artifacts
overwrite: true
@@ -474,12 +474,12 @@ jobs:
exit 1
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Get release artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
@@ -528,12 +528,12 @@ jobs:
exit 1
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Get release artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
@@ -592,12 +592,12 @@ jobs:
exit 1
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Get release artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
@@ -618,20 +618,20 @@ jobs:
echo "Release version: ${{ env.version }}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: env.ACT != 'true'
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push to Docker Hub
uses: docker/build-push-action@v7
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
@@ -651,13 +651,13 @@ jobs:
exit 1
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get bumped Cargo files (Act)
if: env.ACT == 'true'
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
name: bumped-cargo-files
path: ${{ github.workspace }}
@@ -668,13 +668,13 @@ jobs:
git fetch --all
git pull
- uses: actions/cache@v5
- uses: actions/cache@v3
name: Cache Cargo registry
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
- uses: actions/cache@v5
- uses: actions/cache@v3
with:
path: ~/.cargo/bin
key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }}
+5 -5
View File
@@ -33,7 +33,7 @@ jobs:
# toolchain
toolchain: [stable, beta]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install ${{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
@@ -74,7 +74,7 @@ jobs:
# runs-on: ubuntu-latest
# name: ubuntu / stable / minimal-versions
# steps:
# - uses: actions/checkout@v5
# - uses: actions/checkout@v4
# - name: Install Rust stable
# uses: dtolnay/rust-toolchain@stable
@@ -110,7 +110,7 @@ jobs:
# - run: vcpkg install openssl:x64-windows-static-md
# if: runner.os == 'Windows'
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
@@ -151,7 +151,7 @@ jobs:
name: ubuntu / stable / coverage
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
@@ -176,7 +176,7 @@ jobs:
- name: Upload to codecov.io
if: env.ACT != 'true'
uses: codecov/codecov-action@v7
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
-2
View File
@@ -2,5 +2,3 @@
/.idea/
/.scannerwork/
/.act/
/.sisyphus/
/.coyote/
-272
View File
@@ -5,278 +5,6 @@ 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.8.1 (2026-10-12)
### Feat
- all CLI-based errors now come out in the same JSON format to further improve scripting and LLM integration
### Fix
- never panic when the log directory is unwritable; disable file logging instead [#67]
## v0.8.0 (2026-09-30)
### Feat
- show sizes under a gigabyte in megabytes
- **lidarr**: highlight tracks whose album is downloading or awaiting import
- **readarr**: show the author overview in the add author prompt
- **radarr**: render the movie overview popup at the large size
- **radarr**: make the collection movie overview popup scrollable
- **sonarr**: add a scrollable overview popup to series details
- **lidarr**: add a scrollable overview popup to artist details
- **readarr**: add a scrollable overview popup to author details
- **readarr**: add the indexers UI
- **readarr**: add the indexers handlers
- **readarr**: add the system UI
- **readarr**: add the system handlers
- **readarr**: add the root folders UI
- **readarr**: add the add root folder handler
- **readarr**: add the root folders handler
- **readarr**: add the history UI
- **readarr**: add the downloads UI
- **readarr**: add the blocklist UI
- **readarr**: add the history handler
- **readarr**: add the downloads handler
- **readarr**: add the blocklist handler
- **readarr**: add the delete book UI
- **readarr**: add the delete author UI
- **readarr**: add the edit author UI
- **readarr**: add the add author UI
- **readarr**: add the delete book handler
- **readarr**: add the delete author handler
- **readarr**: add the edit author handler
- **readarr**: add the add author handler
- **readarr**: render the edition details popup
- **readarr**: render the book details popup
- **readarr**: reach the book file info tab from the book details tabs
- **readarr**: scroll and dismiss the edition details popup
- **readarr**: handle the book details tabs and open the edition details popup
- **readarr**: render the author details view
- **readarr**: handle key events for the author details view
- **readarr**: handle key events for the authors library table
- **readarr**: fetch data on tick for the active block
- **readarr**: edit the shared indexer settings and expose the command on the CLI
- **readarr**: fetch the shared indexer settings and expose them on the CLI
- **readarr**: test all indexers and expose the command on the CLI
- **readarr**: test an indexer and expose the command on the CLI
- **readarr**: delete an indexer and expose the command on the CLI
- **readarr**: edit an indexer and expose the command on the CLI
- **readarr**: fetch all indexers and expose them on the CLI
- **readarr**: clear the blocklist and expose the command on the CLI
- **readarr**: delete an item from the blocklist and expose the command on the CLI
- **readarr**: fetch the blocklist and expose it on the CLI
- **readarr**: manually download a release and expose the command on the CLI
- **readarr**: fetch releases for an author and expose them on the CLI
- **readarr**: fetch releases for a book and expose them on the CLI
- **readarr**: mark a history item as failed and expose the command on the CLI
- **readarr**: fetch Readarr history and expose it on the CLI
- **readarr**: refresh Readarr downloads and expose the command on the CLI
- **readarr**: delete a download and expose the command on the CLI
- **readarr**: fetch Readarr downloads and expose them on the CLI
- **readarr**: delete a book file and expose the command on the CLI
- **readarr**: delete a book and expose the command on the CLI
- **readarr**: toggle book monitoring and expose the command on the CLI
- **readarr**: trigger an automatic book search and expose the command on the CLI
- **readarr**: fetch Readarr book history and expose it on the CLI
- **readarr**: fetch Readarr book files and expose them on the CLI
- **readarr**: fetch Readarr book editions and expose them on the CLI
- **readarr**: fetch Readarr book details and expose them on the CLI
- **readarr**: fetch Readarr books and expose them on the CLI
- **readarr**: fetch Readarr author history and expose it on the CLI
- **readarr**: refresh all Readarr authors and expose the command on the CLI
- **readarr**: refresh and rescan a Readarr author and expose the command on the CLI
- **readarr**: trigger automatic author searches and expose the command on the CLI
- **readarr**: toggle Readarr author monitoring and expose the command on the CLI
- **readarr**: delete Readarr authors and expose the command on the CLI
- **readarr**: edit Readarr authors and expose the command on the CLI
- **readarr**: add authors to Readarr and expose the command on the CLI
- **readarr**: search for new Readarr authors and expose the command on the CLI
- **readarr**: fetch Readarr author details and expose them on the CLI
- **readarr**: fetch Readarr authors and expose them on the CLI
- **readarr**: delete Readarr root folders and expose the command on the CLI
- **readarr**: add Readarr root folders and expose the command on the CLI
- **readarr**: delete Readarr tags and expose the command on the CLI
- **readarr**: add Readarr tags and expose the command on the CLI
- **readarr**: fetch Readarr root folders and expose them on the CLI
- **readarr**: fetch Readarr tags and expose them on the CLI
- **readarr**: fetch Readarr metadata profiles and expose them on the CLI
- **readarr**: fetch Readarr quality profiles and expose them on the CLI
- **readarr**: fetch Readarr queued events and expose them on the CLI
- **readarr**: trigger Readarr tasks and expose the command on the CLI
- **readarr**: fetch Readarr tasks and expose them on the CLI
- **readarr**: fetch Readarr logs and expose them on the CLI
- **readarr**: fetch Readarr updates and expose them on the CLI
- **readarr**: fetch the Readarr security config and expose it on the CLI
- **readarr**: fetch the Readarr host config and expose it on the CLI
- **readarr**: fetch Readarr disk space and expose it on the CLI
- **readarr**: fetch the Readarr system status and expose it on the CLI
- **readarr**: add the Readarr health check network call
- scaffold Readarr support across the state, app, handler, UI, and CLI layers
- updated gitignore to not include coyote memory or workspace config
### Fix
- **readarr**: send an empty images list when adding an author
- **readarr**: accept release searches wrapped in a filter summary envelope
- **readarr**: tolerate book payloads that omit the grabbed flag
- **radarr**: render the movie overview one size down so the collection details stay visible behind it
- **app/mod**: use correct api for tab when starting on non default tab
- passes servarr-name global switch to both the tui and cli
- **radarr**: model the movie status as an enum
- page up and down stop at the table edges instead of wrapping
- **radarr**: serialize the credit type under the same key it is read from
- **radarr**: model the download status as a DownloadStatus enum
- fall back to the default when a servarr returns an unknown enum value
- show N/A instead of zeros when a season, album or book has no statistics
- stop paging up or down on a single row table from crashing
- keep history readable when an event type is unrecognised
- **readarr**: attach editions when toggling book monitoring
- **sonarr**: stop marking seasons fully downloaded when no episodes are known
- keep a space between overview words that span a line break
- parse the log level from the unscrolled log text
- **radarr**: show a loading block while the updates popup refreshes
- show confirmation prompt clues on delete prompts
- drop redundant references in format arguments
- **readarr**: stop a long author overview hiding the other fields
- **ui**: render detail popup labels in bold
- **readarr**: stop appending stray colons to the edition overview
- **readarr**: move the book file delete to the File tab
- **readarr**: render the update all authors confirmation prompt
- **readarr**: drop the always-empty author type column from the authors table
- tolerate update records that omit the changes list
- address reviewer feedback — rename scroll field to bool, relocate static, drop env var
- revert draw gating; replace frame-count scroll with wall-clock timer
- address reviewer feedback — regex OnceLock and restore tick rate
- reduce TUI idle CPU and improve server error banners
### Refactor
- share one DownloadStatus enum across the servarr models
- move shared servarr types into servarr_models
## v0.7.3 (2026-06-25)
### Feat
- Implemented log rolling so the log file doesn't just grow exponentially [#60]
### Fix
- addressed code review comments
- tail-logs subcommand follows log rollovers and sleeps to minimize idle CPU loops
### Refactor
- Refactored several usages of sort_by_key and match guards to utilize newer Rust version APIs
## 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
-7
View File
@@ -91,12 +91,5 @@ 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, or AI-generated where explicitly disclosed. AI-generated code (e.g., Copilot, ChatGPT,
Claude, etc.) is permitted only when explicitly disclosed and approved. For example: disclosing the use of AI-generated code in a PR description.
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
+997 -1853
View File
File diff suppressed because it is too large. Load diff
+39 -38
View File
@@ -1,6 +1,6 @@
[package]
name = "managarr"
version = "0.8.1"
version = "0.6.3"
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.95.0"
rust-version = "1.89.0"
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
[workspace]
@@ -20,67 +20,68 @@ members = [
]
[dependencies]
anyhow = "1.0.100"
backtrace = "0.3.76"
anyhow = "1.0.68"
backtrace = "0.3.74"
bimap = { version = "0.6.3", features = ["serde"] }
chrono = { version = "0.4.43", features = ["serde"] }
confy = { version = "2.0.0", default-features = false, features = [
chrono = { version = "0.4.38", features = ["serde"] }
confy = { version = "0.6.0", default-features = false, features = [
"yaml_conf",
] }
crossterm = "0.28.1"
derivative = "2.2.0"
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"] }
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"] }
strum = { version = "0.26.3", features = ["derive"] }
strum_macros = "0.26.4"
tokio = { version = "1.49.0", features = ["full"] }
tokio-util = "0.7.18"
ratatui = { version = "0.30.0", features = [
tokio = { version = "1.44.2", features = ["full"] }
tokio-util = "0.7.8"
ratatui = { version = "0.29.0", features = [
"all-widgets",
"unstable-widget-ref",
] }
urlencoding = "2.1.3"
clap = { version = "4.5.56", features = [
urlencoding = "2.1.2"
clap = { version = "4.5.20", features = [
"derive",
"cargo",
"env",
"wrap_help",
] }
clap_complete = "4.5.65"
clap_complete = "4.5.33"
itertools = "0.14.0"
ctrlc = "3.5.1"
colored = "3.1.1"
async-trait = "0.1.89"
ctrlc = "3.4.5"
colored = "3.0.0"
async-trait = "0.1.83"
dirs-next = "2.0.0"
managarr-tree-widget = "0.25.0"
indicatif = "0.17.11"
derive_setters = "0.1.9"
deunicode = "1.6.2"
openssl = { version = "0.10.79", features = ["vendored"] }
managarr-tree-widget = "0.24.0"
indicatif = "0.17.9"
derive_setters = "0.1.6"
deunicode = "1.6.0"
openssl = { version = "0.10.70", features = ["vendored"] }
veil = "0.2.0"
validate_theme_derive = "0.1.0"
enum_display_style_derive = "0.1.0"
tachyonfx = { version = "0.21.0", features = ["sendable"] }
[dev-dependencies]
assert_cmd = "2.1.2"
mockall = "0.13.1"
mockito = "1.7.1"
pretty_assertions = "1.4.1"
proptest = "1.9.0"
assert_cmd = "2.0.16"
mockall = "0.13.0"
mockito = "1.0.0"
pretty_assertions = "1.3.0"
proptest = "1.6.0"
rstest = "0.25.0"
serial_test = "3.3.1"
assertables = "9.8.4"
insta = "1.46.1"
serial_test = "3.2.0"
assertables = "9.8.2"
insta = "1.41.1"
[dev-dependencies.cargo-husky]
version = "1.5.0"
version = "1"
default-features = false
features = ["user-hooks"]
+1 -3
View File
@@ -1,4 +1,4 @@
FROM rust:1.95 AS builder
FROM rust:1.89 AS builder
WORKDIR /usr/src
# Download and compile Rust dependencies in an empty project and cache as a separate Docker layer
@@ -21,8 +21,6 @@ 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
+62 -263
View File
@@ -3,6 +3,7 @@
![Check](https://github.com/Dark-Alex-17/managarr/actions/workflows/check.yml/badge.svg)
![Test](https://github.com/Dark-Alex-17/managarr/actions/workflows/test.yml/badge.svg)
![License](https://img.shields.io/badge/license-MIT-blueviolet.svg)
![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code)
[![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr)
![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff)
[![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr)
@@ -11,26 +12,22 @@
![Docker pulls](https://img.shields.io/docker/pulls/darkalex17/managarr?label=Docker%20downloads)
[![Matrix](https://img.shields.io/matrix/managarr-room%3Amatrix.org?logo=matrix&server_fqdn=matrix.org&fetchMode=guest&style=social&label=Managarr%20Matrix%20Space&link=https%3A%2F%2Fmatrix.to%2F%23%2F%23managarr%3Amatrix.org)](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!
![library](screenshots/lidarr/lidarr_library.png)
![library](screenshots/sonarr/sonarr_library.png)
## What Servarrs are supported?
- [x] ![radarr_logo](logos/radarr.png) [Radarr](https://wiki.servarr.com/radarr)
- [x] ![sonarr_logo](logos/sonarr.png) [Sonarr](https://wiki.servarr.com/en/sonarr)
- [x] ![lidarr_logo](logos/lidarr.png) [Lidarr](https://wiki.servarr.com/en/lidarr)
- [x] ![readarr_logo](logos/readarr.png) [Readarr](https://wiki.servarr.com/en/readarr)
- [ ] ![readarr_logo](logos/readarr.png) [Readarr](https://wiki.servarr.com/en/readarr)
- [ ] ![lidarr_logo](logos/lidarr.png) [Lidarr](https://wiki.servarr.com/en/lidarr)
- [ ] ![prowlarr_logo](logos/prowlarr.png) [Prowlarr](https://wiki.servarr.com/en/prowlarr)
- [ ] ![whisparr_logo](logos/whisparr.png) [Whisparr](https://wiki.servarr.com/whisparr)
- [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/)
- [ ] ![tautulli_logo](logos/tautulli.png) [Tautulli](https://tautulli.com/)
> [!NOTE]
> The official Readarr repository has been archived, but Managarr's Readarr support is compatible with any Readarr forks
> that maintain compatibility with the official Readarr v1 API. See [Tested Readarr Forks](#tested-readarr-forks) below
> for forks that have been explicitly tested with Managarr, and any oddities in configuring them to work with Managarr.
## Try Out the Demo
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
Simply run the following command to start a demo:
@@ -59,9 +56,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 `just build-docker` to build a docker image locally and run it using the above command.
You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command.
Please note that you will need to create and populate your configuration file first before starting the container. Otherwise, the container will fail to start.
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.
**Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path.
@@ -99,7 +96,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.7.0
choco install managarr --version=0.5.0
```
To upgrade to the latest and greatest version of Managarr:
@@ -107,7 +104,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.7.0
choco upgrade managarr --version=0.5.0
```
### Manual
@@ -185,45 +182,13 @@ 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
| TUI | CLI | Feature |
|-----|-----|----------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, editions |
| ✅ | ✅ | View details of a specific author, book, or edition 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 authors to your library |
| ✅ | ✅ | Delete authors, books, downloads, indexers, root folders, and book files |
| ✅ | ✅ | Trigger automatic searches for authors or books |
| ✅ | ✅ | Trigger refresh and disk scan for authors and downloads |
| ✅ | ✅ | Manually search for author releases or books |
| ✅ | ✅ | Edit your authors 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 |
- [ ] Support for Readarr
### Lidarr
- [ ] Support for Lidarr
### Whisparr
@@ -266,7 +231,7 @@ To see all available commands, simply run `managarr --help`:
```shell
$ managarr --help
managarr 0.8.0
managarr 0.5.1
Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs
@@ -276,36 +241,20 @@ 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
readarr Commands for managing your Readarr 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:
@@ -334,21 +283,12 @@ 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:
-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.
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
-h, --help Print help
```
**Pro Tip:** The CLI is even more powerful and useful when used in conjunction with the `jq` CLI tool. This allows you to parse the JSON response from the Managarr CLI and use it in your scripts; For example, to extract the `movieId` of the movie "Ad Astra", you would run:
@@ -360,49 +300,23 @@ $ 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. 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:
but all servers will require you to input the API token.
| Servarr | Default Host | Default Port |
|---------|--------------|--------------|
| Radarr | `localhost` | 7878 |
| Sonarr | `localhost` | 8989 |
| Lidarr | `localhost` | 8686 |
| Readarr | `localhost` | 8787 |
The configuration file is located somewhere different for each OS.
> [!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>
readarr:
# Connect to readarr running on localhost:8787
- api_token: <your-readarr-api-token-here>
### Linux
```
$HOME/.config/managarr/config.yml
```
## Configuration File Location
### Mac
```
$HOME/Library/Application Support/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
### Windows
```
%APPDATA%/Roaming/managarr/config.yml
```
## Specify Which Configuration File to Use
@@ -421,107 +335,43 @@ radarr:
- host: 192.168.0.78
port: 7878
api_token: someApiToken1234567890
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
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
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
- uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
api_token: someApiToken1234567890
- name: Anime Sonarr # An example of a custom name for a secondary Sonarr instance
host: 192.168.1.89
host: 192.168.0.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
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
whisparr:
- host: 192.168.0.69
port: 6969
api_token: someApiToken1234567890
ssl_cert_path: /path/to/lidarr_1.crt
ssl_cert_path: /path/to/whisparr.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}
readarr:
- name: Chaptarr (Ebooks) # Example of how to use the Readarr support with a Readarr fork like Chaptarr
uri: http://192.168.0.87:8789/readarr/gr/ebook
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:
@@ -578,45 +428,8 @@ 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` |
## Tested Readarr Forks
The following Readarr forks have been explicitly tested with Managarr:
* [Bookshelf](https://github.com/pennydreadful/bookshelf)
* [Readarr-Resurrected](https://github.com/ricetim/readarr-rresurrected)
* [Librarr](https://github.com/Rorqualx/Librarr)
* [Chaptarr](https://github.com/Chaptarr/Chaptarr)
> [!IMPORTANT]
> Chaptarr houses a dedicated Readarr facade middleware that activates on a path prefix, meaning a bare config doesn't
> work. Instead, users need to configure both Audiobooks and Ebooks separately, and based on their desired metadata
> fetcher (i.e. Goodreads and Hardcover IDs). Because of this, Chaptarr-based configurations must use the `uri` key
> rather than the `host` and `port` keys. The below configuration file demonstrates all the supported variations for
> connecting Managarr to Chaptarr.
### Chaptarr Example Configuration
```yaml
readarr:
# Because Chaptarr supports both eBooks and audiobooks, the same instance must be provisioned twice if you're using
# both so Managarr can differentiate between the two. Thus, for Chaptarr, you need an eBook server configured and an
# audiobook server configured like below.
- name: eBooks (Goodreads) # Readarr originally used Goodreads as its metadata provider
uri: http://192.168.0.87:8789/readarr/gr/ebook # Note the `gr` in the path prefix indicating "GoodReads"
api_token: someApiToken1234567890
- name: Chaptarr Audiobooks (Goodreads)
uri: http://192.168.0.105:8789/readarr/gr/audiobook
api_token: someApiToken1234567890
- name: Chaptarr eBooks (Hardcover)
uri: http://192.168.0.105:8789/readarr/hc/ebook # Note the `hc` in the path prefix indicating "HardCover"
api_token: someApiToken1234567890
- name: Chaptarr Audiobooks (Hardcover)
uri: http://192.168.0.105:8789/readarr/hc/audiobook
api_token: someApiToken1234567890
```
## 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
@@ -633,20 +446,6 @@ readarr:
![season_details](screenshots/sonarr/season_details.png)
![manual_episode_search](screenshots/sonarr/manual_episode_search.png)
### Lidarr
![lidarr_library](screenshots/lidarr/lidarr_library.png)
![artist_details](screenshots/lidarr/artist_details.png)
![album_details](screenshots/lidarr/album_details.png)
![artist_discography_search](screenshots/lidarr/artist_discography_search.png)
![manual_album_search](screenshots/lidarr/manual_album_search.png)
### Readarr
![readarr_library](screenshots/readarr/readarr_library.png)
![author_details](screenshots/readarr/author_details.png)
![book_details](screenshots/readarr/book_details.png)
![manual_author_search](screenshots/readarr/manual_author_search.png)
![manual_album_search](screenshots/readarr/manual_book_search.png)
### General
![logs](screenshots/radarr/logs.png)
![indexers](screenshots/radarr/indexers.png)
@@ -662,8 +461,8 @@ readarr:
## Servarr Requirements
* [Radarr >= 5.3.6.8612](https://radarr.video/docs/api/)
* [Sonarr >= v4](https://sonarr.tv/docs/api/)
* [Lidarr v1](https://lidarr.audio/docs/api/)
* [Readarr v1](https://readarr.com/docs/api/)
* [Lidarr v1](https://lidarr.audio/docs/api/)
* [Whisparr >= v3](https://whisparr.com/docs/api/)
* [Prowlarr v1](https://prowlarr.com/docs/api/)
* [Bazarr v1.1.4](http://localhost:6767/api)
+5 -3
View File
@@ -1,5 +1,7 @@
VERSION := "latest"
IMG_NAME := "darkalex17/managarr"
IMAGE := "{{IMG_NAME}}:{{VERSION}}"
# List all recipes
default:
@@ -49,7 +51,7 @@ doctest:
# Run all proptests
[group: 'test']
proptest:
@cargo test -- property_tests proptest
@cargo test proptest
alias test-snapshots := snapshot-tests
# Run all snapshot tests
@@ -85,5 +87,5 @@ build build_type='debug':
# Build the docker image
[group: 'build']
build-docker version=VERSION:
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMG_NAME}}:{{version}} .
build-docker:
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMAGE}}
@@ -5,4 +5,3 @@
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 56330c025ad79db641d0eb9f429ab74e95822e1fb015b58f0e158ea674cd42a1 # shrinks to list_size = 1, page_ops = 1
cc 29df5ff471eb388b0ffde573a959a539e4cf32acd9e1cea5187bba2268dd57f7 # shrinks to list_size = 1, filter_term = "𑛀"
+1 -1
View File
@@ -1,5 +1,5 @@
tab_spaces=2
edition = "2024"
edition = "2021"
reorder_imports = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

+13 -192
View File
@@ -5,18 +5,14 @@ mod tests {
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde_json::Value;
use serial_test::serial;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::{HorizontallyScrollableText, TabRoute};
use crate::network::NetworkEvent;
use crate::network::radarr_network::RadarrEvent;
use crate::network::sonarr_network::SonarrEvent;
use tokio_util::sync::CancellationToken;
#[test]
@@ -35,18 +31,10 @@ mod tests {
..ServarrConfig::default()
};
let sonarr_config_2 = ServarrConfig::default();
let readarr_config_1 = ServarrConfig {
name: Some("Readarr Test".to_owned()),
weight: Some(2),
..ServarrConfig::default()
};
let readarr_config_2 = ServarrConfig::default();
let config = AppConfig {
theme: None,
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
lidarr: None,
readarr: Some(vec![readarr_config_1.clone(), readarr_config_2.clone()]),
};
let expected_tab_routes = vec![
TabRoute {
@@ -55,12 +43,6 @@ mod tests {
contextual_help: None,
config: Some(sonarr_config_1),
},
TabRoute {
title: "Readarr Test".to_owned(),
route: ActiveReadarrBlock::default().into(),
contextual_help: None,
config: Some(readarr_config_1),
},
TabRoute {
title: "Radarr 1".to_owned(),
route: ActiveRadarrBlock::default().into(),
@@ -79,12 +61,6 @@ mod tests {
contextual_help: None,
config: Some(sonarr_config_2),
},
TabRoute {
title: "Readarr 1".to_owned(),
route: ActiveReadarrBlock::default().into(),
contextual_help: None,
config: Some(readarr_config_2),
},
];
let app = App::new(
@@ -102,9 +78,9 @@ mod tests {
assert_eq!(app.server_tabs.index, 0);
assert_eq!(app.server_tabs.tabs, expected_tab_routes);
assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.scroll_interval, Duration::from_millis(100));
assert_eq!(app.ticks_until_scroll, 4);
assert_eq!(app.tick_count, 0);
assert!(!app.should_text_scroll);
assert_eq!(app.ui_scroll_tick_count, 0);
assert!(!app.is_loading);
assert!(!app.is_routing);
assert!(!app.should_refresh);
@@ -123,7 +99,7 @@ mod tests {
assert_eq!(app.error, HorizontallyScrollableText::default());
assert_eq!(app.server_tabs.index, 0);
assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.scroll_interval, Duration::from_millis(100));
assert_eq!(app.ticks_until_scroll, 4);
assert_eq!(app.tick_count, 0);
assert!(!app.is_loading);
assert!(!app.is_routing);
@@ -166,36 +142,6 @@ mod tests {
assert!(app.is_routing);
}
#[tokio::test]
async fn test_named_tab_uses_selected_servarr_route_and_network_events() {
let (network_tx, mut network_rx) = mpsc::channel(500);
let mut app = App::new(
network_tx,
AppConfig {
radarr: Some(vec![ServarrConfig {
name: Some("Movies".to_owned()),
..ServarrConfig::default()
}]),
sonarr: Some(vec![ServarrConfig {
name: Some("Shows".to_owned()),
..ServarrConfig::default()
}]),
..AppConfig::default()
},
CancellationToken::new(),
);
assert!(app.server_tabs.select_tab_by_title("Shows"));
assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into());
app.on_tick().await;
assert_eq!(
network_rx.recv().await.unwrap(),
SonarrEvent::GetQualityProfiles.into()
);
}
#[test]
fn test_reset_cancellation_token() {
let mut app = App {
@@ -238,9 +184,7 @@ mod tests {
..SonarrData::default()
};
let data = Data {
lidarr_data: LidarrData::default(),
radarr_data,
readarr_data: ReadarrData::default(),
sonarr_data,
};
let mut app = App {
@@ -300,20 +244,22 @@ mod tests {
#[test]
fn test_on_ui_scroll_tick() {
let mut app = App {
scroll_interval: Duration::ZERO,
ticks_until_scroll: 1,
..App::default()
};
app.on_ui_scroll_tick();
assert!(app.should_text_scroll);
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
app.on_ui_scroll_tick();
assert!(app.should_text_scroll);
app.scroll_interval = Duration::from_secs(60);
app.last_scroll = Instant::now();
assert_eq!(app.ui_scroll_tick_count, 1);
assert_eq!(app.tick_count, 0);
app.on_ui_scroll_tick();
assert!(!app.should_text_scroll);
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
@@ -498,78 +444,6 @@ 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() {
@@ -630,56 +504,6 @@ 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() {
@@ -793,11 +617,10 @@ 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: Some(true), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}"
"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\"}}) }}"
);
let servarr_config = ServarrConfig {
name: Some(name),
@@ -808,9 +631,7 @@ 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);
-21
View File
@@ -1,8 +1,6 @@
use crate::app::App;
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
use crate::app::readarr::readarr_context_clues::ReadarrContextClueProvider;
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
use crate::models::Route;
@@ -23,8 +21,6 @@ impl ContextClueProvider for ServarrContextClueProvider {
match app.get_current_route() {
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app),
Route::Readarr(_, _) => ReadarrContextClueProvider::get_context_clues(app),
_ => None,
}
}
@@ -104,18 +100,6 @@ pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
),
];
pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
(DEFAULT_KEYBINDINGS.tasks, "open tasks"),
(DEFAULT_KEYBINDINGS.events, "open events"),
@@ -126,8 +110,3 @@ pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
+10 -64
View File
@@ -2,14 +2,13 @@
mod test {
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
SYSTEM_TASKS_CONTEXT_CLUES, ServarrContextClueProvider,
ServarrContextClueProvider,
};
use crate::app::{App, key_binding::DEFAULT_KEYBINDINGS};
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
#[test]
@@ -205,40 +204,6 @@ mod test {
assert_none!(indexers_context_clues_iter.next());
}
#[test]
fn test_history_context_clues() {
let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_none!(history_context_clues_iter.next());
}
#[test]
fn test_system_context_clues() {
let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter();
@@ -269,21 +234,6 @@ mod test {
assert_none!(system_context_clues_iter.next());
}
#[test]
fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
system_tasks_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "start task")
);
assert_some_eq_x!(
system_tasks_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(system_tasks_context_clues_iter.next());
}
#[test]
fn test_servarr_context_clue_provider_delegates_to_radarr_provider() {
let mut app = App::test_default();
@@ -291,7 +241,10 @@ mod test {
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
assert_some_eq_x!(
context_clues,
&crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
);
}
#[test]
@@ -301,17 +254,10 @@ mod test {
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
}
#[test]
fn test_servarr_context_clue_provider_delegates_to_readarr_provider() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::SystemTasks.into());
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
assert_some_eq_x!(
context_clues,
&crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
);
}
#[test]
-6
View File
@@ -27,7 +27,6 @@ generate_keybindings! {
filter,
sort,
edit,
view,
logs,
tasks,
test,
@@ -143,11 +142,6 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
alt: None,
desc: "edit",
},
view: KeyBinding {
key: Key::Char('v'),
alt: None,
desc: "view",
},
events: KeyBinding {
key: Key::Char('e'),
alt: None,
-1
View File
@@ -26,7 +26,6 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), None, "filter")]
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")]
#[case(DEFAULT_KEYBINDINGS.view, Key::Char('v'), None, "view")]
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), None, "events")]
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), None, "logs")]
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), None, "tasks")]
-242
View File
@@ -1,242 +0,0 @@
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
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,
ARTIST_OVERVIEW_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS,
EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, TRACK_DETAILS_BLOCKS,
};
#[cfg(test)]
#[path = "lidarr_context_clues_tests.rs"]
mod lidarr_context_clues_tests;
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 10] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.update, "update all"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.delete, "delete album"),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
"toggle album monitoring",
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.view, "view overview"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static ARTIST_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static ARTIST_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_ARTIST_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static ALBUM_DETAILS_CONTEXT_CLUES: [ContextClue; 6] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
(DEFAULT_KEYBINDINGS.submit, "track details"),
(DEFAULT_KEYBINDINGS.delete, "delete track"),
];
pub static ALBUM_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
(
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.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_ALBUM_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(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 {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() else {
panic!("LidarrContextClueProvider::get_context_clues called with non-Lidarr route");
};
match active_lidarr_block {
_ if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
.data
.lidarr_data
.artist_info_tabs
.get_active_route_contextual_help(),
_ if ARTIST_OVERVIEW_BLOCKS.contains(&active_lidarr_block) => {
Some(&ARTIST_OVERVIEW_CONTEXT_CLUES)
}
_ if ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
.data
.lidarr_data
.album_details_modal
.as_ref()
.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
| ActiveLidarrBlock::SystemLogs
| ActiveLidarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
_ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block)
|| EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block)
|| INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block)
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
_ if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block)
|| DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveLidarrBlock::AddArtistPrompt
| ActiveLidarrBlock::AddArtistSelectMonitor
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
| ActiveLidarrBlock::AddArtistSelectQualityProfile
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
| ActiveLidarrBlock::AddArtistSelectRootFolder
| ActiveLidarrBlock::AddArtistTagsInput
| ActiveLidarrBlock::AddArtistAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
_ if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
Some(&ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES)
}
ActiveLidarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
_ => app
.data
.lidarr_data
.main_tabs
.get_active_route_contextual_help(),
}
}
}
@@ -1,658 +0,0 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::lidarr::lidarr_context_clues::{
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ALBUM_DETAILS_CONTEXT_CLUES,
ALBUM_HISTORY_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES,
ARTIST_OVERVIEW_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
MANUAL_ALBUM_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, TrackDetailsModal};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use rstest::rstest;
#[test]
fn test_artists_context_clues() {
let mut artists_context_clues_iter = ARTISTS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, "update all")
);
assert_some_eq_x!(
artists_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_none!(artists_context_clues_iter.next());
}
#[test]
fn test_artist_details_context_clues() {
let mut artist_details_context_clues_iter = ARTIST_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete album")
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
"toggle album monitoring",
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
)
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.view, "view overview")
);
assert_some_eq_x!(
artist_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(artist_details_context_clues_iter.next());
}
#[test]
fn test_artist_overview_context_clues() {
let mut artist_overview_context_clues_iter = ARTIST_OVERVIEW_CONTEXT_CLUES.iter();
assert_some_eq_x!(
artist_overview_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
artist_overview_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(artist_overview_context_clues_iter.next());
}
#[test]
fn test_add_artist_search_results_context_clues() {
let mut add_artist_search_results_context_clues_iter =
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
add_artist_search_results_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
add_artist_search_results_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "edit search")
);
assert_none!(add_artist_search_results_context_clues_iter.next());
}
#[test]
#[should_panic(
expected = "LidarrContextClueProvider::get_context_clues called with non-Lidarr route"
)]
fn test_lidarr_context_clue_provider_get_context_clues_non_lidarr_route() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::default().into());
LidarrContextClueProvider::get_context_clues(&mut app);
}
#[test]
fn test_artist_history_context_clues() {
let mut artist_history_context_clues_iter = ARTIST_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
artist_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(artist_history_context_clues_iter.next());
}
#[test]
fn test_manual_artist_search_context_clues() {
let mut manual_artist_search_context_clues_iter = MANUAL_ARTIST_SEARCH_CONTEXT_CLUES.iter();
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_artist_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(manual_artist_search_context_clues_iter.next());
}
#[test]
fn test_album_details_context_clues() {
let mut album_details_context_clues_iter = ALBUM_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "track details")
);
assert_some_eq_x!(
album_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete track")
);
assert_none!(album_details_context_clues_iter.next());
}
#[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(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
album_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(album_history_context_clues_iter.next());
}
#[test]
fn test_manual_album_search_context_clues() {
let mut manual_album_search_context_clues_iter = MANUAL_ALBUM_SEARCH_CONTEXT_CLUES.iter();
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_album_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
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)]
#[case(2, ActiveLidarrBlock::ManualArtistSearch, &MANUAL_ARTIST_SEARCH_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_artist_info_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.lidarr_data = LidarrData::default();
app.data.lidarr_data.artist_info_tabs.set_index(index);
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);
}
#[rstest]
#[case(0, ActiveLidarrBlock::AlbumDetails, &ALBUM_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveLidarrBlock::AlbumHistory, &ALBUM_HISTORY_CONTEXT_CLUES)]
#[case(2, ActiveLidarrBlock::ManualAlbumSearch, &MANUAL_ALBUM_SEARCH_CONTEXT_CLUES)]
fn test_lidarr_context_clue_provider_album_details_tabs(
#[case] index: usize,
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
let mut album_details_modal = AlbumDetailsModal::default();
album_details_modal.album_details_tabs.set_index(index);
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);
}
#[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_artist_overview_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistOverview.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &ARTIST_OVERVIEW_CONTEXT_CLUES);
}
#[test]
fn test_lidarr_context_clue_provider_artists_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
}
#[test]
fn test_lidarr_context_clue_provider_artists_sort_prompt_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::ArtistsSortPrompt.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
}
#[test]
fn test_lidarr_context_clue_provider_search_artists_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::SearchArtists.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
}
#[test]
fn test_lidarr_context_clue_provider_filter_artists_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::FilterArtists.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
}
#[rstest]
fn test_lidarr_context_clue_provider_bare_popup_context_clues(
#[values(
ActiveLidarrBlock::AddArtistSearchInput,
ActiveLidarrBlock::AddArtistEmptySearchResults,
ActiveLidarrBlock::TestAllIndexers
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES);
}
#[test]
fn test_lidarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() {
let mut blocks = EDIT_ARTIST_BLOCKS.to_vec();
blocks.extend(ADD_ROOT_FOLDER_BLOCKS);
blocks.extend(INDEXER_SETTINGS_BLOCKS);
blocks.extend(EDIT_INDEXER_BLOCKS);
for active_lidarr_block in blocks {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
}
#[test]
fn test_lidarr_context_clue_provider_add_artist_search_results_context_clues() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES);
}
#[rstest]
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_add_artist_blocks(
#[values(
ActiveLidarrBlock::AddArtistPrompt,
ActiveLidarrBlock::AddArtistSelectMonitor,
ActiveLidarrBlock::AddArtistSelectMonitorNewItems,
ActiveLidarrBlock::AddArtistSelectQualityProfile,
ActiveLidarrBlock::AddArtistSelectMetadataProfile,
ActiveLidarrBlock::AddArtistSelectRootFolder,
ActiveLidarrBlock::AddArtistTagsInput,
ActiveLidarrBlock::AddArtistAlreadyInLibrary
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_delete_artist_blocks(
#[values(
ActiveLidarrBlock::DeleteArtistPrompt,
ActiveLidarrBlock::DeleteArtistConfirmPrompt,
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion,
ActiveLidarrBlock::DeleteArtistToggleDeleteFile
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_delete_album_blocks(
#[values(
ActiveLidarrBlock::DeleteAlbumPrompt,
ActiveLidarrBlock::DeleteAlbumConfirmPrompt,
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion,
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[test]
fn test_lidarr_context_clue_provider_system_tasks_clues() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into());
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES);
}
}
-783
View File
@@ -1,783 +0,0 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::models::lidarr_models::{Album, Artist, LidarrRelease};
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
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::{
album, artist, track,
};
use pretty_assertions::{assert_eq, assert_str_eq};
use tokio::sync::mpsc;
#[tokio::test]
async fn test_dispatch_by_lidarr_block_artists() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Artists)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetQualityProfiles.into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetMetadataProfiles.into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_lidarr_block_artist_details() {
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::ArtistDetails)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into());
assert!(!app.data.lidarr_data.prompt_confirm);
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);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistHistory)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetArtistHistory(1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_artist_search_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDiscographyReleases(1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_artist_search_block_discography_releases_non_empty() {
let mut app = App::test_default();
app
.data
.lidarr_data
.discography_releases
.set_items(vec![LidarrRelease::default()]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_album_details_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app.data.lidarr_data.albums.set_items(vec![Album {
id: 1,
..Album::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumDetails)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTracks(1, 1).into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetTrackFiles(1).into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_album_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app.data.lidarr_data.albums.set_items(vec![Album {
id: 1,
..Album::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumHistory)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetAlbumHistory(1, 1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_album_history_block_no_op_when_albums_table_is_empty() {
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumHistory)
.await;
assert!(!app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_album_search_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.artists.set_items(vec![Artist {
id: 1,
..Artist::default()
}]);
app.data.lidarr_data.albums.set_items(vec![Album {
id: 1,
..Album::default()
}]);
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetAlbumReleases(1, 1).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_album_search_block_is_loading() {
let mut app = App {
is_loading: true,
..App::test_default()
};
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
.await;
assert!(app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_album_search_block_album_releases_non_empty() {
let mut app = App::test_default();
let mut album_details_modal = AlbumDetailsModal::default();
album_details_modal
.album_releases
.set_items(vec![LidarrRelease::default()]);
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_downloads_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Downloads)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.add_artist_search = Some("test artist".into());
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AddArtistSearchResults)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::SearchNewArtist("test artist".to_owned()).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::History)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetHistory(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_root_folders_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::RootFolders)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_indexers_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Indexers)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetIndexers.into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_all_indexer_settings_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AllIndexerSettingsPrompt)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetAllIndexerSettings.into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_test_indexer_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.indexers.set_items(vec![Indexer {
id: 1,
..Indexer::default()
}]);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TestIndexer)
.await;
assert!(app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::TestIndexer(1).into());
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_test_all_indexers_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TestAllIndexers)
.await;
assert!(app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::TestAllIndexers.into()
);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::System)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTasks.into());
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetQueuedEvents.into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetLogs(500).into());
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_updates_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::SystemUpdates)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetUpdates.into());
assert!(!app.data.lidarr_data.prompt_confirm);
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(5).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, 5).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();
app.data.lidarr_data.prompt_confirm = false;
app.check_for_lidarr_prompt_action().await;
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.should_refresh);
}
#[tokio::test]
async fn test_check_for_lidarr_prompt_action() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::GetStatus);
app.check_for_lidarr_prompt_action().await;
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
assert!(app.should_refresh);
assert_eq!(app.data.lidarr_data.prompt_confirm_action, None);
}
#[tokio::test]
async fn test_lidarr_refresh_metadata() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.refresh_lidarr_metadata().await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetQualityProfiles.into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetMetadataProfiles.into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
assert!(app.is_loading);
}
#[tokio::test]
async fn test_lidarr_on_tick_first_render() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_first_render = true;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetQualityProfiles.into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetMetadataProfiles.into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
assert!(app.is_loading);
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.is_first_render);
}
#[tokio::test]
async fn test_lidarr_on_tick_routing() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_lidarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.should_refresh = false;
app.is_first_render = false;
app.tick_count = 1;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert!(app.cancellation_token.is_cancelled());
}
#[tokio::test]
async fn test_lidarr_on_tick_should_refresh() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(app.should_refresh);
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_lidarr_on_tick_should_refresh_does_not_cancel_prompt_requests() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_loading = true;
app.is_routing = true;
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
assert!(!app.data.lidarr_data.prompt_confirm);
assert!(!app.cancellation_token.is_cancelled());
}
#[tokio::test]
async fn test_lidarr_on_tick_network_tick_frequency() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.tick_count = 2;
app.tick_until_poll = 2;
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetQualityProfiles.into()
);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetMetadataProfiles.into()
);
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
}
#[tokio::test]
async fn test_extract_add_new_artist_search_query() {
let app = App::test_default_fully_populated();
let query = app.extract_add_new_artist_search_query().await;
assert_str_eq!(query, "Test Artist");
}
#[tokio::test]
#[should_panic(expected = "Add artist search is empty")]
async fn test_extract_add_new_artist_search_query_panics_when_the_query_is_not_set() {
let app = App::test_default();
app.extract_add_new_artist_search_query().await;
}
#[tokio::test]
async fn test_extract_artist_id() {
let mut app = App::test_default();
app.data.lidarr_data.artists.set_items(vec![artist()]);
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, 5);
}
#[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();
app.data.lidarr_data.indexers.set_items(vec![Indexer {
id: 1,
..Indexer::default()
}]);
assert_eq!(app.extract_lidarr_indexer_id().await, 1);
}
}
-279
View File
@@ -1,279 +0,0 @@
use super::App;
use crate::{
models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
network::lidarr_network::LidarrEvent,
};
pub mod lidarr_context_clues;
#[cfg(test)]
#[path = "lidarr_tests.rs"]
mod lidarr_tests;
impl App<'_> {
pub(super) async fn dispatch_by_lidarr_block(&mut self, active_lidarr_block: &ActiveLidarrBlock) {
match active_lidarr_block {
ActiveLidarrBlock::Artists => {
self
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetTags.into())
.await;
self
.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())
.await;
}
ActiveLidarrBlock::ArtistDetails => {
self
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
.await;
}
ActiveLidarrBlock::ArtistHistory => {
self
.dispatch_network_event(
LidarrEvent::GetArtistHistory(self.extract_artist_id().await).into(),
)
.await;
}
ActiveLidarrBlock::ManualArtistSearch => {
if self.data.lidarr_data.discography_releases.is_empty() {
self
.dispatch_network_event(
LidarrEvent::GetDiscographyReleases(self.extract_artist_id().await).into(),
)
.await;
}
}
ActiveLidarrBlock::AlbumDetails => {
let artist_id = self.extract_artist_id().await;
let album_id = self.extract_album_id().await;
self
.dispatch_network_event(LidarrEvent::GetTracks(artist_id, album_id).into())
.await;
self
.dispatch_network_event(LidarrEvent::GetTrackFiles(album_id).into())
.await;
self
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
.await;
}
ActiveLidarrBlock::AlbumHistory => {
if !self.data.lidarr_data.albums.is_empty() {
self
.dispatch_network_event(
LidarrEvent::GetAlbumHistory(
self.extract_artist_id().await,
self.extract_album_id().await,
)
.into(),
)
.await;
}
}
ActiveLidarrBlock::ManualAlbumSearch => {
match self.data.lidarr_data.album_details_modal.as_ref() {
Some(album_details_modal) if album_details_modal.album_releases.is_empty() => {
self
.dispatch_network_event(
LidarrEvent::GetAlbumReleases(
self.extract_artist_id().await,
self.extract_album_id().await,
)
.into(),
)
.await;
}
_ => (),
}
}
ActiveLidarrBlock::AddArtistSearchResults => {
self
.dispatch_network_event(
LidarrEvent::SearchNewArtist(self.extract_add_new_artist_search_query().await).into(),
)
.await;
}
ActiveLidarrBlock::History => {
self
.dispatch_network_event(LidarrEvent::GetHistory(500).into())
.await
}
ActiveLidarrBlock::RootFolders => {
self
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
.await;
}
ActiveLidarrBlock::Indexers => {
self
.dispatch_network_event(LidarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetIndexers.into())
.await;
}
ActiveLidarrBlock::AllIndexerSettingsPrompt => {
self
.dispatch_network_event(LidarrEvent::GetAllIndexerSettings.into())
.await;
}
ActiveLidarrBlock::TestIndexer => {
self
.dispatch_network_event(
LidarrEvent::TestIndexer(self.extract_lidarr_indexer_id().await).into(),
)
.await;
}
ActiveLidarrBlock::TestAllIndexers => {
self
.dispatch_network_event(LidarrEvent::TestAllIndexers.into())
.await;
}
ActiveLidarrBlock::System => {
self
.dispatch_network_event(LidarrEvent::GetTasks.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetQueuedEvents.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetLogs(500).into())
.await;
}
ActiveLidarrBlock::SystemUpdates => {
self
.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;
}
_ => (),
}
self.check_for_lidarr_prompt_action().await;
self.reset_tick_count();
}
async fn extract_add_new_artist_search_query(&self) -> String {
self
.data
.lidarr_data
.add_artist_search
.as_ref()
.expect("Add artist search is empty")
.text
.clone()
}
async fn extract_artist_id(&self) -> i64 {
self.data.lidarr_data.artists.current_selection().id
}
async fn extract_album_id(&self) -> i64 {
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
}
async fn check_for_lidarr_prompt_action(&mut self) {
if self.data.lidarr_data.prompt_confirm {
self.data.lidarr_data.prompt_confirm = false;
if let Some(lidarr_event) = self.data.lidarr_data.prompt_confirm_action.take() {
self.dispatch_network_event(lidarr_event.into()).await;
self.should_refresh = true;
}
}
}
pub(super) async fn lidarr_on_tick(&mut self, active_lidarr_block: ActiveLidarrBlock) {
if self.is_first_render {
self.refresh_lidarr_metadata().await;
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
self.is_first_render = false;
return;
}
if self.should_refresh {
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
self.refresh_lidarr_metadata().await;
}
if self.is_routing {
if !self.should_refresh {
self.cancellation_token.cancel();
} else {
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
}
}
if self.tick_count.is_multiple_of(self.tick_until_poll) {
self.refresh_lidarr_metadata().await;
}
}
async fn refresh_lidarr_metadata(&mut self) {
self
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
.await;
self
.dispatch_network_event(LidarrEvent::GetDiskSpace.into())
.await;
self
.dispatch_network_event(LidarrEvent::GetStatus.into())
.await;
}
}
+29 -184
View File
@@ -7,17 +7,14 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use std::{fs, process};
use tachyonfx::{Duration, EffectManager};
use tokio::sync::mpsc::Sender;
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::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
@@ -29,9 +26,7 @@ mod app_tests;
pub mod context_clues;
pub mod key_binding;
mod key_binding_tests;
pub mod lidarr;
pub mod radarr;
pub mod readarr;
pub mod sonarr;
pub struct App<'a> {
@@ -42,12 +37,13 @@ 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 scroll_interval: Duration,
pub last_scroll: Instant,
pub ticks_until_scroll: u64,
pub tick_count: u64,
pub should_text_scroll: bool,
pub ui_scroll_tick_count: u64,
pub last_tick: Duration,
pub effects: EffectManager<()>,
pub has_active_effect: bool,
pub is_routing: bool,
pub is_loading: bool,
pub should_refresh: bool,
@@ -104,46 +100,6 @@ impl App<'_> {
server_tabs.extend(sonarr_tabs);
}
if let Some(lidarr_configs) = config.lidarr {
let mut unnamed_idx = 0;
let lidarr_tabs = lidarr_configs.into_iter().map(|lidarr_config| {
let name = if let Some(name) = lidarr_config.name.clone() {
name
} else {
unnamed_idx += 1;
format!("Lidarr {unnamed_idx}")
};
TabRoute {
title: name,
route: ActiveLidarrBlock::Artists.into(),
contextual_help: None,
config: Some(lidarr_config),
}
});
server_tabs.extend(lidarr_tabs);
}
if let Some(readarr_configs) = config.readarr {
let mut unnamed_idx = 0;
let readarr_tabs = readarr_configs.into_iter().map(|readarr_config| {
let name = if let Some(name) = readarr_config.name.clone() {
name
} else {
unnamed_idx += 1;
format!("Readarr {unnamed_idx}")
};
TabRoute {
title: name,
route: ActiveReadarrBlock::Authors.into(),
contextual_help: None,
config: Some(readarr_config),
}
});
server_tabs.extend(readarr_tabs);
}
let weight_sorted_tabs = server_tabs
.into_iter()
.sorted_by(|tab1, tab2| {
@@ -195,11 +151,10 @@ impl App<'_> {
}
pub fn on_ui_scroll_tick(&mut self) {
if self.last_scroll.elapsed() >= self.scroll_interval {
self.should_text_scroll = true;
self.last_scroll = Instant::now();
if self.ui_scroll_tick_count == self.ticks_until_scroll {
self.ui_scroll_tick_count = 0;
} else {
self.should_text_scroll = false;
self.ui_scroll_tick_count += 1;
}
}
@@ -225,8 +180,6 @@ impl App<'_> {
match self.get_current_route() {
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await,
Route::Readarr(active_readarr_block, _) => self.readarr_on_tick(active_readarr_block).await,
_ => (),
}
@@ -240,6 +193,7 @@ impl App<'_> {
pub fn push_navigation_stack(&mut self, route: Route) {
self.navigation_stack.push(route);
self.is_routing = true;
self.has_active_effect = false;
}
pub fn pop_navigation_stack(&mut self) {
@@ -263,11 +217,14 @@ impl App<'_> {
}
pub fn get_current_route(&self) -> Route {
self
.navigation_stack
.last()
.copied()
.unwrap_or_else(|| self.server_tabs.get_active_route())
*self.navigation_stack.last().unwrap_or(
&self
.server_tabs
.tabs
.first()
.expect("At least one server tab must exist")
.route,
)
}
}
@@ -279,14 +236,15 @@ 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,
scroll_interval: Duration::from_millis(100),
last_scroll: Instant::now(),
ticks_until_scroll: 4,
tick_count: 0,
should_text_scroll: false,
ui_scroll_tick_count: 0,
last_tick: Duration::ZERO,
effects: EffectManager::default(),
has_active_effect: false,
is_loading: false,
is_routing: false,
should_refresh: false,
@@ -314,29 +272,15 @@ impl App<'_> {
contextual_help: None,
config: Some(ServarrConfig::default()),
},
TabRoute {
title: "Lidarr".to_owned(),
route: ActiveLidarrBlock::Artists.into(),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
TabRoute {
title: "Readarr".to_owned(),
route: ActiveReadarrBlock::Authors.into(),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
]),
..App::default()
}
}
pub fn test_default_fully_populated() -> Self {
App {
data: Data {
lidarr_data: LidarrData::test_default_fully_populated(),
radarr_data: RadarrData::test_default_fully_populated(),
readarr_data: ReadarrData::test_default_fully_populated(),
sonarr_data: SonarrData::test_default_fully_populated(),
},
server_tabs: TabState::new(vec![
@@ -352,55 +296,31 @@ impl App<'_> {
contextual_help: None,
config: Some(ServarrConfig::default()),
},
TabRoute {
title: "Lidarr".to_owned(),
route: ActiveLidarrBlock::Artists.into(),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
TabRoute {
title: "Readarr".to_owned(),
route: ActiveReadarrBlock::Authors.into(),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
]),
..App::default()
}
}
pub fn get_navigation_stack_depth(&self) -> usize {
self.navigation_stack.len()
}
}
#[derive(Default)]
pub struct Data<'a> {
pub lidarr_data: LidarrData<'a>,
pub radarr_data: RadarrData<'a>,
pub readarr_data: ReadarrData<'a>,
pub sonarr_data: SonarrData<'a>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AppConfig {
pub theme: Option<String>,
pub lidarr: Option<Vec<ServarrConfig>>,
pub radarr: Option<Vec<ServarrConfig>>,
pub readarr: Option<Vec<ServarrConfig>>,
pub sonarr: Option<Vec<ServarrConfig>>,
}
impl AppConfig {
pub fn validate(&self, config_path: &str) {
if self.lidarr.is_none()
&& self.radarr.is_none()
&& self.sonarr.is_none()
&& self.readarr.is_none()
{
log_and_print_error(format!(
"No Servarrs are configured in the file: {config_path}"
));
pub fn validate(&self) {
if self.radarr.is_none() && self.sonarr.is_none() {
log_and_print_error(
"No Servarr configuration provided in the specified configuration file".to_owned(),
);
process::exit(1);
}
@@ -411,14 +331,6 @@ impl AppConfig {
if let Some(sonarr_configs) = &self.sonarr {
sonarr_configs.iter().for_each(|config| config.validate());
}
if let Some(lidarr_configs) = &self.lidarr {
lidarr_configs.iter().for_each(|config| config.validate());
}
if let Some(readarr_configs) = &self.readarr {
readarr_configs.iter().for_each(|config| config.validate());
}
}
pub fn verify_config_present_for_cli(&self, command: &Command) {
@@ -436,14 +348,6 @@ impl AppConfig {
msg("Sonarr");
process::exit(1);
}
Command::Lidarr(_) if self.lidarr.is_none() => {
msg("Lidarr");
process::exit(1);
}
Command::Readarr(_) if self.readarr.is_none() => {
msg("Readarr");
process::exit(1);
}
_ => (),
}
}
@@ -460,18 +364,6 @@ impl AppConfig {
sonarr_config.post_process_initialization();
}
}
if let Some(lidarr_configs) = self.lidarr.as_mut() {
for lidarr_config in lidarr_configs {
lidarr_config.post_process_initialization();
}
}
if let Some(readarr_configs) = self.readarr.as_mut() {
for readarr_config in readarr_configs {
readarr_config.post_process_initialization();
}
}
}
}
@@ -492,8 +384,6 @@ 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(
@@ -502,8 +392,6 @@ 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 {
@@ -549,9 +437,7 @@ 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,
}
}
}
@@ -596,29 +482,6 @@ 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>
@@ -641,24 +504,6 @@ 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>,
-5
View File
@@ -62,11 +62,6 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await;
}
ActiveRadarrBlock::History => {
self
.dispatch_network_event(RadarrEvent::GetHistory(500).into())
.await;
}
ActiveRadarrBlock::Indexers => {
self
.dispatch_network_event(RadarrEvent::GetTags.into())
+7 -6
View File
@@ -1,13 +1,12 @@
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::models::Route;
use crate::models::servarr_data::radarr::radarr_data::{
ADD_MOVIE_BLOCKS, ActiveRadarrBlock, DELETE_MOVIE_BLOCKS, EDIT_COLLECTION_BLOCKS,
EDIT_INDEXER_BLOCKS, EDIT_MOVIE_BLOCKS, INDEXER_SETTINGS_BLOCKS, MOVIE_DETAILS_BLOCKS,
ADD_MOVIE_BLOCKS, ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS, EDIT_INDEXER_BLOCKS,
EDIT_MOVIE_BLOCKS, INDEXER_SETTINGS_BLOCKS, MOVIE_DETAILS_BLOCKS,
};
#[cfg(test)]
@@ -83,6 +82,11 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.submit, "show overview/add movie"),
(DEFAULT_KEYBINDINGS.edit, "edit collection"),
@@ -120,9 +124,6 @@ impl ContextClueProvider for RadarrContextClueProvider {
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
_ if DELETE_MOVIE_BLOCKS.contains(&active_radarr_block) => {
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveRadarrBlock::AddMoviePrompt
| ActiveRadarrBlock::AddMovieSelectMonitor
| ActiveRadarrBlock::AddMovieSelectMinimumAvailability
+6 -27
View File
@@ -3,15 +3,14 @@ mod tests {
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
SYSTEM_TASKS_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES,
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider,
MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider, SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
@@ -415,25 +414,6 @@ mod tests {
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_delete_movie_blocks(
#[values(
ActiveRadarrBlock::DeleteMoviePrompt,
ActiveRadarrBlock::DeleteMovieConfirmPrompt,
ActiveRadarrBlock::DeleteMovieToggleAddListExclusion,
ActiveRadarrBlock::DeleteMovieToggleDeleteFile
)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_radarr_context_clue_provider_add_movie_search_results_context_clues(
#[values(
@@ -479,10 +459,9 @@ mod tests {
#[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)]
#[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)]
#[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::History, &HISTORY_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
#[case(6, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
#[case(7, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
#[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
fn test_radarr_context_clue_provider_radarr_blocks_context_clues(
#[case] index: usize,
#[case] active_radarr_block: ActiveRadarrBlock,
+3 -21
View File
@@ -6,8 +6,7 @@ mod tests {
use crate::app::App;
use crate::app::radarr::ActiveRadarrBlock;
use crate::models::radarr_models::{
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, MinimumAvailability, Movie,
MovieMonitor, RadarrRelease,
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease,
};
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_models::Indexer;
@@ -89,13 +88,13 @@ mod tests {
tmdb_id: 1234,
title: "Test".to_owned(),
root_folder_path: "/nfs2".to_owned(),
minimum_availability: MinimumAvailability::Announced,
minimum_availability: "announced".to_owned(),
monitored: true,
quality_profile_id: 2222,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddMovieOptions {
monitor: MovieMonitor::MovieOnly,
monitor: "movieOnly".to_owned(),
search_for_movie: true,
},
};
@@ -147,23 +146,6 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_history_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_radarr_block(&ActiveRadarrBlock::History)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetHistory(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_root_folders_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
-248
View File
@@ -1,248 +0,0 @@
use super::App;
use crate::{
models::servarr_data::readarr::readarr_data::ActiveReadarrBlock,
network::readarr_network::ReadarrEvent,
};
pub mod readarr_context_clues;
#[cfg(test)]
#[path = "readarr_tests.rs"]
mod readarr_tests;
impl App<'_> {
pub(super) async fn dispatch_by_readarr_block(
&mut self,
active_readarr_block: &ActiveReadarrBlock,
) {
match active_readarr_block {
ActiveReadarrBlock::Authors => {
self
.dispatch_network_event(ReadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetMetadataProfiles.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(ReadarrEvent::ListAuthors.into())
.await;
}
ActiveReadarrBlock::Blocklist => {
self
.dispatch_network_event(ReadarrEvent::GetBlocklist.into())
.await;
}
ActiveReadarrBlock::Downloads => {
self
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
.await;
}
ActiveReadarrBlock::AuthorDetails => {
self
.dispatch_network_event(ReadarrEvent::GetBooks(self.extract_author_id().await).into())
.await;
}
ActiveReadarrBlock::AuthorHistory => {
self
.dispatch_network_event(
ReadarrEvent::GetAuthorHistory(self.extract_author_id().await).into(),
)
.await;
}
ActiveReadarrBlock::ManualAuthorSearch => {
if self.data.readarr_data.author_releases.is_empty() {
self
.dispatch_network_event(
ReadarrEvent::GetAuthorReleases(self.extract_author_id().await).into(),
)
.await;
}
}
ActiveReadarrBlock::BookDetails => {
let book_id = self.extract_book_id().await;
self
.dispatch_network_event(ReadarrEvent::GetBookEditions(book_id).into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetBookFiles(book_id).into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
.await;
}
ActiveReadarrBlock::BookHistory => {
if !self.data.readarr_data.books.is_empty() {
self
.dispatch_network_event(
ReadarrEvent::GetBookHistory(
self.extract_author_id().await,
self.extract_book_id().await,
)
.into(),
)
.await;
}
}
ActiveReadarrBlock::ManualBookSearch => {
match self.data.readarr_data.book_details_modal.as_ref() {
Some(book_details_modal) if book_details_modal.book_releases.is_empty() => {
self
.dispatch_network_event(
ReadarrEvent::GetBookReleases(self.extract_book_id().await).into(),
)
.await;
}
_ => (),
}
}
ActiveReadarrBlock::AddAuthorSearchResults => {
self
.dispatch_network_event(
ReadarrEvent::SearchNewAuthor(self.extract_add_new_author_search_query().await).into(),
)
.await;
}
ActiveReadarrBlock::History => {
self
.dispatch_network_event(ReadarrEvent::GetHistory(500).into())
.await
}
ActiveReadarrBlock::RootFolders => {
self
.dispatch_network_event(ReadarrEvent::GetRootFolders.into())
.await;
}
ActiveReadarrBlock::Indexers => {
self
.dispatch_network_event(ReadarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetIndexers.into())
.await;
}
ActiveReadarrBlock::AllIndexerSettingsPrompt => {
self
.dispatch_network_event(ReadarrEvent::GetAllIndexerSettings.into())
.await;
}
ActiveReadarrBlock::TestIndexer => {
self
.dispatch_network_event(
ReadarrEvent::TestIndexer(self.extract_readarr_indexer_id().await).into(),
)
.await;
}
ActiveReadarrBlock::TestAllIndexers => {
self
.dispatch_network_event(ReadarrEvent::TestAllIndexers.into())
.await;
}
ActiveReadarrBlock::System => {
self
.dispatch_network_event(ReadarrEvent::GetTasks.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetQueuedEvents.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetLogs(500).into())
.await;
}
ActiveReadarrBlock::SystemUpdates => {
self
.dispatch_network_event(ReadarrEvent::GetUpdates.into())
.await;
}
_ => (),
}
self.check_for_readarr_prompt_action().await;
self.reset_tick_count();
}
async fn extract_add_new_author_search_query(&self) -> String {
self
.data
.readarr_data
.add_author_search
.as_ref()
.expect("Add author search is empty")
.text
.clone()
}
async fn extract_author_id(&self) -> i64 {
self.data.readarr_data.authors.current_selection().id
}
async fn extract_book_id(&self) -> i64 {
self.data.readarr_data.books.current_selection().id
}
async fn extract_readarr_indexer_id(&self) -> i64 {
self.data.readarr_data.indexers.current_selection().id
}
async fn check_for_readarr_prompt_action(&mut self) {
if self.data.readarr_data.prompt_confirm {
self.data.readarr_data.prompt_confirm = false;
if let Some(readarr_event) = self.data.readarr_data.prompt_confirm_action.take() {
self.dispatch_network_event(readarr_event.into()).await;
self.should_refresh = true;
}
}
}
pub(super) async fn readarr_on_tick(&mut self, active_readarr_block: ActiveReadarrBlock) {
if self.is_first_render {
self.refresh_readarr_metadata().await;
self.dispatch_by_readarr_block(&active_readarr_block).await;
self.is_first_render = false;
return;
}
if self.should_refresh {
self.dispatch_by_readarr_block(&active_readarr_block).await;
self.refresh_readarr_metadata().await;
}
if self.is_routing {
if !self.should_refresh {
self.cancellation_token.cancel();
} else {
self.dispatch_by_readarr_block(&active_readarr_block).await;
}
}
if self.tick_count.is_multiple_of(self.tick_until_poll) {
self.refresh_readarr_metadata().await;
}
}
async fn refresh_readarr_metadata(&mut self) {
self
.dispatch_network_event(ReadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetMetadataProfiles.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetDiskSpace.into())
.await;
self
.dispatch_network_event(ReadarrEvent::GetStatus.into())
.await;
}
}
-230
View File
@@ -1,230 +0,0 @@
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::models::Route;
use crate::models::servarr_data::readarr::readarr_data::{
ADD_AUTHOR_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS,
ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS,
EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS, EDITION_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
};
#[cfg(test)]
#[path = "readarr_context_clues_tests.rs"]
mod readarr_context_clues_tests;
pub static AUTHORS_CONTEXT_CLUES: [ContextClue; 10] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.update, "update all"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static AUTHOR_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit author"),
(DEFAULT_KEYBINDINGS.delete, "delete book"),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
"toggle book monitoring",
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.view, "view overview"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static AUTHOR_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static AUTHOR_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit author"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, "edit author"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static BOOK_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
(DEFAULT_KEYBINDINGS.submit, "edition details"),
];
pub static BOOK_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
(
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.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static BOOK_FILE_CONTEXT_CLUES: [ContextClue; 3] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.delete, "delete book file"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static MANUAL_BOOK_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static EDITION_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub(in crate::app) struct ReadarrContextClueProvider;
impl ContextClueProvider for ReadarrContextClueProvider {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
let Route::Readarr(active_readarr_block, _context_option) = app.get_current_route() else {
panic!("ReadarrContextClueProvider::get_context_clues called with non-Readarr route");
};
match active_readarr_block {
_ if AUTHOR_DETAILS_BLOCKS.contains(&active_readarr_block) => app
.data
.readarr_data
.author_info_tabs
.get_active_route_contextual_help(),
_ if AUTHOR_OVERVIEW_BLOCKS.contains(&active_readarr_block) => {
Some(&AUTHOR_OVERVIEW_CONTEXT_CLUES)
}
_ if BOOK_DETAILS_BLOCKS.contains(&active_readarr_block) => app
.data
.readarr_data
.book_details_modal
.as_ref()
.expect("book_details_modal is empty")
.book_details_tabs
.get_active_route_contextual_help(),
_ if EDITION_DETAILS_BLOCKS.contains(&active_readarr_block) => {
Some(&EDITION_DETAILS_CONTEXT_CLUES)
}
ActiveReadarrBlock::AddAuthorSearchInput
| ActiveReadarrBlock::AddAuthorEmptySearchResults
| ActiveReadarrBlock::TestAllIndexers
| ActiveReadarrBlock::SystemLogs
| ActiveReadarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
_ if EDIT_AUTHOR_BLOCKS.contains(&active_readarr_block)
|| EDIT_INDEXER_BLOCKS.contains(&active_readarr_block)
|| INDEXER_SETTINGS_BLOCKS.contains(&active_readarr_block)
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_readarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
_ if DELETE_AUTHOR_BLOCKS.contains(&active_readarr_block)
|| DELETE_BOOK_BLOCKS.contains(&active_readarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveReadarrBlock::AddAuthorPrompt
| ActiveReadarrBlock::AddAuthorSelectMonitor
| ActiveReadarrBlock::AddAuthorSelectMonitorNewItems
| ActiveReadarrBlock::AddAuthorSelectQualityProfile
| ActiveReadarrBlock::AddAuthorSelectMetadataProfile
| ActiveReadarrBlock::AddAuthorSelectRootFolder
| ActiveReadarrBlock::AddAuthorTagsInput
| ActiveReadarrBlock::AddAuthorAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
_ if ADD_AUTHOR_BLOCKS.contains(&active_readarr_block) => {
Some(&ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES)
}
ActiveReadarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
_ => app
.data
.readarr_data
.main_tabs
.get_active_route_contextual_help(),
}
}
}
@@ -1,628 +0,0 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::readarr::readarr_context_clues::{
ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES, AUTHOR_DETAILS_CONTEXT_CLUES,
AUTHOR_HISTORY_CONTEXT_CLUES, AUTHOR_OVERVIEW_CONTEXT_CLUES, AUTHORS_CONTEXT_CLUES,
BOOK_DETAILS_CONTEXT_CLUES, BOOK_FILE_CONTEXT_CLUES, BOOK_HISTORY_CONTEXT_CLUES,
EDITION_DETAILS_CONTEXT_CLUES, MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES,
MANUAL_BOOK_SEARCH_CONTEXT_CLUES, ReadarrContextClueProvider,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::readarr::modals::BookDetailsModal;
use crate::models::servarr_data::readarr::readarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ActiveReadarrBlock, EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS,
INDEXER_SETTINGS_BLOCKS, ReadarrData,
};
use rstest::rstest;
#[test]
fn test_authors_context_clues() {
let mut authors_context_clues_iter = AUTHORS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
)
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, "update all")
);
assert_some_eq_x!(
authors_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_none!(authors_context_clues_iter.next());
}
#[test]
fn test_author_details_context_clues() {
let mut author_details_context_clues_iter = AUTHOR_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
)
);
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit author")
);
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete book")
);
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
"toggle book monitoring",
)
);
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
)
);
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.view, "view overview")
);
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(author_details_context_clues_iter.next());
}
#[test]
fn test_author_overview_context_clues() {
let mut author_overview_context_clues_iter = AUTHOR_OVERVIEW_CONTEXT_CLUES.iter();
assert_some_eq_x!(
author_overview_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
author_overview_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(author_overview_context_clues_iter.next());
}
#[test]
fn test_add_author_search_results_context_clues() {
let mut add_author_search_results_context_clues_iter =
ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
add_author_search_results_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
add_author_search_results_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "edit search")
);
assert_none!(add_author_search_results_context_clues_iter.next());
}
#[test]
#[should_panic(
expected = "ReadarrContextClueProvider::get_context_clues called with non-Readarr route"
)]
fn test_readarr_context_clue_provider_get_context_clues_non_readarr_route() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::default().into());
ReadarrContextClueProvider::get_context_clues(&mut app);
}
#[test]
fn test_author_history_context_clues() {
let mut author_history_context_clues_iter = AUTHOR_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit author")
);
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
author_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(author_history_context_clues_iter.next());
}
#[test]
fn test_manual_author_search_context_clues() {
let mut manual_author_search_context_clues_iter = MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES.iter();
assert_some_eq_x!(
manual_author_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_author_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit author")
);
assert_some_eq_x!(
manual_author_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_author_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
manual_author_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_author_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_author_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(manual_author_search_context_clues_iter.next());
}
#[test]
fn test_book_details_context_clues() {
let mut book_details_context_clues_iter = BOOK_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
book_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
book_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
book_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
book_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_some_eq_x!(
book_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "edition details")
);
assert_none!(book_details_context_clues_iter.next());
}
#[test]
fn test_book_history_context_clues() {
let mut book_history_context_clues_iter = BOOK_HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
book_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
book_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
book_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
book_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
book_history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
book_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
book_history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
);
assert_none!(book_history_context_clues_iter.next());
}
#[test]
fn test_book_file_context_clues() {
let mut book_file_context_clues_iter = BOOK_FILE_CONTEXT_CLUES.iter();
assert_some_eq_x!(
book_file_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
book_file_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, "delete book file")
);
assert_some_eq_x!(
book_file_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(book_file_context_clues_iter.next());
}
#[test]
fn test_manual_book_search_context_clues() {
let mut manual_book_search_context_clues_iter = MANUAL_BOOK_SEARCH_CONTEXT_CLUES.iter();
assert_some_eq_x!(
manual_book_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_book_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_book_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_book_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_book_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(manual_book_search_context_clues_iter.next());
}
#[test]
fn test_edition_details_context_clues() {
let mut edition_details_context_clues_iter = EDITION_DETAILS_CONTEXT_CLUES.iter();
assert_some_eq_x!(
edition_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
edition_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(edition_details_context_clues_iter.next());
}
#[rstest]
#[case(0, ActiveReadarrBlock::AuthorDetails, &AUTHOR_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveReadarrBlock::AuthorHistory, &AUTHOR_HISTORY_CONTEXT_CLUES)]
#[case(2, ActiveReadarrBlock::ManualAuthorSearch, &MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES)]
fn test_readarr_context_clue_provider_author_info_tabs(
#[case] index: usize,
#[case] active_readarr_block: ActiveReadarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.readarr_data = ReadarrData::default();
app.data.readarr_data.author_info_tabs.set_index(index);
app.push_navigation_stack(active_readarr_block.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[rstest]
#[case(0, ActiveReadarrBlock::BookDetails, &BOOK_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveReadarrBlock::BookHistory, &BOOK_HISTORY_CONTEXT_CLUES)]
#[case(2, ActiveReadarrBlock::BookFileInfo, &BOOK_FILE_CONTEXT_CLUES)]
#[case(3, ActiveReadarrBlock::ManualBookSearch, &MANUAL_BOOK_SEARCH_CONTEXT_CLUES)]
fn test_readarr_context_clue_provider_book_details_tabs(
#[case] index: usize,
#[case] active_readarr_block: ActiveReadarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
let mut book_details_modal = BookDetailsModal::default();
book_details_modal.book_details_tabs.set_index(index);
let readarr_data = ReadarrData {
book_details_modal: Some(book_details_modal),
..ReadarrData::default()
};
app.data.readarr_data = readarr_data;
app.push_navigation_stack(active_readarr_block.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[test]
fn test_readarr_context_clue_provider_book_edition_details_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::BookEditionDetails.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &EDITION_DETAILS_CONTEXT_CLUES);
}
#[test]
fn test_readarr_context_clue_provider_author_overview_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &AUTHOR_OVERVIEW_CONTEXT_CLUES);
}
#[test]
fn test_readarr_context_clue_provider_authors_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::Authors.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
}
#[test]
fn test_readarr_context_clue_provider_authors_sort_prompt_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::AuthorsSortPrompt.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
}
#[test]
fn test_readarr_context_clue_provider_search_authors_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::SearchAuthors.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
}
#[test]
fn test_readarr_context_clue_provider_filter_authors_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::FilterAuthors.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
}
#[rstest]
fn test_readarr_context_clue_provider_bare_popup_context_clues(
#[values(
ActiveReadarrBlock::AddAuthorSearchInput,
ActiveReadarrBlock::AddAuthorEmptySearchResults,
ActiveReadarrBlock::TestAllIndexers,
ActiveReadarrBlock::SystemLogs,
ActiveReadarrBlock::SystemUpdates
)]
active_readarr_block: ActiveReadarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_readarr_block.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES);
}
#[test]
fn test_readarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() {
let mut blocks = EDIT_AUTHOR_BLOCKS.to_vec();
blocks.extend(ADD_ROOT_FOLDER_BLOCKS);
blocks.extend(INDEXER_SETTINGS_BLOCKS);
blocks.extend(EDIT_INDEXER_BLOCKS);
for active_readarr_block in blocks {
let mut app = App::test_default();
app.push_navigation_stack(active_readarr_block.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
}
#[test]
fn test_readarr_context_clue_provider_add_author_search_results_context_clues() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::AddAuthorSearchResults.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES);
}
#[rstest]
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_add_author_blocks(
#[values(
ActiveReadarrBlock::AddAuthorPrompt,
ActiveReadarrBlock::AddAuthorSelectMonitor,
ActiveReadarrBlock::AddAuthorSelectMonitorNewItems,
ActiveReadarrBlock::AddAuthorSelectQualityProfile,
ActiveReadarrBlock::AddAuthorSelectMetadataProfile,
ActiveReadarrBlock::AddAuthorSelectRootFolder,
ActiveReadarrBlock::AddAuthorTagsInput,
ActiveReadarrBlock::AddAuthorAlreadyInLibrary
)]
active_readarr_block: ActiveReadarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_readarr_block.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_delete_author_blocks(
#[values(
ActiveReadarrBlock::DeleteAuthorPrompt,
ActiveReadarrBlock::DeleteAuthorConfirmPrompt,
ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion,
ActiveReadarrBlock::DeleteAuthorToggleDeleteFile
)]
active_readarr_block: ActiveReadarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_readarr_block.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_delete_book_blocks(
#[values(
ActiveReadarrBlock::DeleteBookPrompt,
ActiveReadarrBlock::DeleteBookConfirmPrompt,
ActiveReadarrBlock::DeleteBookToggleAddListExclusion,
ActiveReadarrBlock::DeleteBookToggleDeleteFile
)]
active_readarr_block: ActiveReadarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_readarr_block.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[test]
fn test_readarr_context_clue_provider_system_tasks_clues() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::SystemTasks.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES);
}
}
-798
View File
@@ -1,798 +0,0 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::models::readarr_models::{Author, Book, ReadarrRelease};
use crate::models::servarr_data::readarr::modals::BookDetailsModal;
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
use crate::models::servarr_models::Indexer;
use crate::network::NetworkEvent;
use crate::network::readarr_network::ReadarrEvent;
use pretty_assertions::{assert_eq, assert_str_eq};
use tokio::sync::mpsc;
#[tokio::test]
async fn test_dispatch_by_readarr_block_authors() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetMetadataProfiles.into()
);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::ListAuthors.into());
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_readarr_block_author_details() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.data.readarr_data.authors.set_items(vec![Author {
id: 1,
..Author::default()
}]);
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::AuthorDetails)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetBooks(1).into());
assert!(!app.data.readarr_data.prompt_confirm);
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.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::Blocklist)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetBlocklist.into());
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_author_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.authors.set_items(vec![Author {
id: 1,
..Author::default()
}]);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::AuthorHistory)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetAuthorHistory(1).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_author_search_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.authors.set_items(vec![Author {
id: 1,
..Author::default()
}]);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualAuthorSearch)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetAuthorReleases(1).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_author_search_block_author_releases_non_empty() {
let mut app = App::test_default();
app
.data
.readarr_data
.author_releases
.set_items(vec![ReadarrRelease::default()]);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualAuthorSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_book_details_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.authors.set_items(vec![Author {
id: 1,
..Author::default()
}]);
app.data.readarr_data.books.set_items(vec![Book {
id: 2,
..Book::default()
}]);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookDetails)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetBookEditions(2).into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetBookFiles(2).into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_book_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.authors.set_items(vec![Author {
id: 1,
..Author::default()
}]);
app.data.readarr_data.books.set_items(vec![Book {
id: 2,
..Book::default()
}]);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookHistory)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetBookHistory(1, 2).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_book_history_block_no_op_when_books_table_is_empty() {
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.authors.set_items(vec![Author {
id: 1,
..Author::default()
}]);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookHistory)
.await;
assert!(!app.is_loading);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_book_search_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.authors.set_items(vec![Author {
id: 1,
..Author::default()
}]);
app.data.readarr_data.books.set_items(vec![Book {
id: 2,
..Book::default()
}]);
app.data.readarr_data.book_details_modal = Some(BookDetailsModal::default());
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetBookReleases(2).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_book_search_block_is_loading() {
let mut app = App {
is_loading: true,
..App::test_default()
};
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
.await;
assert!(app.is_loading);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_book_search_block_book_releases_non_empty() {
let mut app = App::test_default();
let mut book_details_modal = BookDetailsModal::default();
book_details_modal
.book_releases
.set_items(vec![ReadarrRelease::default()]);
app.data.readarr_data.book_details_modal = Some(book_details_modal);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_book_edition_details_block_does_not_dispatch() {
let mut app = App::test_default_fully_populated();
app.data.readarr_data.prompt_confirm = false;
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookEditionDetails)
.await;
assert!(!app.is_loading);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_downloads_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::Downloads)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_readarr_block_add_author_search_results() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.add_author_search = Some("test author".into());
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::AddAuthorSearchResults)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::SearchNewAuthor("test author".to_owned()).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::History)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetHistory(500).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_root_folders_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::RootFolders)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetRootFolders.into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_indexers_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::Indexers)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetIndexers.into());
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_all_indexer_settings_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::AllIndexerSettingsPrompt)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetAllIndexerSettings.into()
);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_test_indexer_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.indexers.set_items(vec![Indexer {
id: 3,
..Indexer::default()
}]);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::TestIndexer)
.await;
assert!(app.is_loading);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::TestIndexer(3).into()
);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_test_all_indexers_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::TestAllIndexers)
.await;
assert!(app.is_loading);
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::TestAllIndexers.into()
);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::System)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTasks.into());
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetQueuedEvents.into()
);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetLogs(500).into());
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_updates_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::SystemUpdates)
.await;
assert!(app.is_loading);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetUpdates.into());
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_readarr_block_resets_the_tick_count() {
let mut app = App {
tick_count: 2,
..App::test_default()
};
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
.await;
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_readarr_block_clears_the_pending_prompt_confirmation() {
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
.await;
assert!(!app.data.readarr_data.prompt_confirm);
assert!(!app.should_refresh);
}
#[tokio::test]
async fn test_check_for_readarr_prompt_action_no_prompt_confirm() {
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = false;
app.check_for_readarr_prompt_action().await;
assert!(!app.data.readarr_data.prompt_confirm);
assert!(!app.should_refresh);
}
#[tokio::test]
async fn test_check_for_readarr_prompt_action_with_no_prompt_confirm_action() {
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.check_for_readarr_prompt_action().await;
assert!(!app.data.readarr_data.prompt_confirm);
assert_none!(app.data.readarr_data.prompt_confirm_action);
assert!(!app.should_refresh);
}
#[tokio::test]
async fn test_check_for_readarr_prompt_action() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::GetStatus);
app.check_for_readarr_prompt_action().await;
assert!(!app.data.readarr_data.prompt_confirm);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
assert!(app.should_refresh);
assert_eq!(app.data.readarr_data.prompt_confirm_action, None);
}
#[tokio::test]
async fn test_readarr_refresh_metadata() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.refresh_readarr_metadata().await;
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetMetadataProfiles.into()
);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetRootFolders.into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetDownloads(500).into()
);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetDiskSpace.into());
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
assert!(app.is_loading);
}
#[tokio::test]
async fn test_readarr_on_tick_first_render() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_first_render = true;
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetMetadataProfiles.into()
);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetRootFolders.into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetDownloads(500).into()
);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetDiskSpace.into());
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
assert!(app.is_loading);
assert!(!app.data.readarr_data.prompt_confirm);
assert!(!app.is_first_render);
}
#[tokio::test]
async fn test_readarr_on_tick_routing() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.readarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_readarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_routing = true;
app.should_refresh = false;
app.is_first_render = false;
app.tick_count = 1;
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
assert!(app.cancellation_token.is_cancelled());
}
#[tokio::test]
async fn test_readarr_on_tick_should_refresh() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetDownloads(500).into()
);
assert!(app.should_refresh);
assert!(!app.data.readarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_readarr_on_tick_should_refresh_does_not_cancel_prompt_requests() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_loading = true;
app.is_routing = true;
app.should_refresh = true;
app.is_first_render = false;
app.tick_count = 1;
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
assert!(!app.data.readarr_data.prompt_confirm);
assert!(!app.cancellation_token.is_cancelled());
}
#[tokio::test]
async fn test_readarr_on_tick_network_tick_frequency() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.readarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.is_first_render = false;
app.tick_count = 2;
app.tick_until_poll = 2;
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetMetadataProfiles.into()
);
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetRootFolders.into()
);
assert_eq!(
rx.recv().await.unwrap(),
ReadarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
}
#[tokio::test]
async fn test_extract_add_new_author_search_query() {
let app = App::test_default_fully_populated();
let query = app.extract_add_new_author_search_query().await;
assert_str_eq!(query, "Test Author");
}
#[tokio::test]
#[should_panic(expected = "Add author search is empty")]
async fn test_extract_add_new_author_search_query_panics_when_the_query_is_not_set() {
let app = App::test_default();
app.extract_add_new_author_search_query().await;
}
#[tokio::test]
async fn test_extract_author_id() {
let mut app = App::test_default();
app.data.readarr_data.authors.set_items(vec![Author {
id: 1,
..Author::default()
}]);
assert_eq!(app.extract_author_id().await, 1);
}
#[tokio::test]
async fn test_extract_book_id() {
let mut app = App::test_default();
app.data.readarr_data.books.set_items(vec![Book {
id: 2,
..Book::default()
}]);
assert_eq!(app.extract_book_id().await, 2);
}
#[tokio::test]
async fn test_extract_readarr_indexer_id() {
let mut app = App::test_default();
app.data.readarr_data.indexers.set_items(vec![Indexer {
id: 3,
..Indexer::default()
}]);
assert_eq!(app.extract_readarr_indexer_id().await, 3);
}
}
+6 -4
View File
@@ -58,19 +58,21 @@ 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(series_id, season_number).into())
.dispatch_network_event(
SonarrEvent::GetSeasonHistory(self.extract_series_id_season_number_tuple().await)
.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(series_id, season_number).into(),
SonarrEvent::GetSeasonReleases(self.extract_series_id_season_number_tuple().await)
.into(),
)
.await;
}
+14 -14
View File
@@ -1,13 +1,11 @@
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider,
SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::app::{App, context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS};
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{
ADD_SERIES_BLOCKS, ActiveSonarrBlock, DELETE_SERIES_BLOCKS, EDIT_INDEXER_BLOCKS,
EDIT_SERIES_BLOCKS, EPISODE_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, SEASON_DETAILS_BLOCKS,
SERIES_DETAILS_BLOCKS, SERIES_OVERVIEW_BLOCKS,
ADD_SERIES_BLOCKS, ActiveSonarrBlock, EDIT_INDEXER_BLOCKS, EDIT_SERIES_BLOCKS,
EPISODE_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS,
};
#[cfg(test)]
@@ -39,7 +37,7 @@ pub static SERIES_CONTEXT_CLUES: [ContextClue; 11] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
@@ -56,16 +54,19 @@ pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.view, "view overview"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static SERIES_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
@@ -174,6 +175,11 @@ pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub(in crate::app) struct SonarrContextClueProvider;
impl ContextClueProvider for SonarrContextClueProvider {
@@ -187,9 +193,6 @@ impl ContextClueProvider for SonarrContextClueProvider {
.sonarr_data
.series_info_tabs
.get_active_route_contextual_help(),
_ if SERIES_OVERVIEW_BLOCKS.contains(&active_sonarr_block) => {
Some(&SERIES_OVERVIEW_CONTEXT_CLUES)
}
_ if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
.data
.sonarr_data
@@ -220,9 +223,6 @@ impl ContextClueProvider for SonarrContextClueProvider {
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
_ if DELETE_SERIES_BLOCKS.contains(&active_sonarr_block) => {
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveSonarrBlock::AddSeriesPrompt
| ActiveSonarrBlock::AddSeriesSelectMonitor
| ActiveSonarrBlock::AddSeriesSelectSeriesType
+41 -56
View File
@@ -2,9 +2,8 @@
mod tests {
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
SYSTEM_TASKS_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::sonarr::sonarr_context_clues::{
SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, SonarrContextClueProvider,
@@ -14,9 +13,10 @@ mod tests {
key_binding::DEFAULT_KEYBINDINGS,
sonarr::sonarr_context_clues::{
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES,
SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES,
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SERIES_OVERVIEW_CONTEXT_CLUES,
HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES,
SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
},
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
@@ -146,6 +146,40 @@ mod tests {
assert_none!(series_history_context_clues_iter.next());
}
#[test]
fn test_history_context_clues() {
let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter();
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
history_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_none!(history_context_clues_iter.next());
}
#[test]
fn test_series_details_context_clues() {
let mut series_details_context_clues_iter = SERIES_DETAILS_CONTEXT_CLUES.iter();
@@ -187,10 +221,6 @@ mod tests {
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
series_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.view, "view overview")
);
assert_some_eq_x!(
series_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
@@ -198,24 +228,6 @@ mod tests {
assert_none!(series_details_context_clues_iter.next());
}
#[test]
fn test_series_overview_context_clues() {
let mut series_overview_context_clues_iter = SERIES_OVERVIEW_CONTEXT_CLUES.iter();
assert_some_eq_x!(
series_overview_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
series_overview_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(series_overview_context_clues_iter.next());
}
#[test]
fn test_season_details_context_clues() {
let mut season_details_context_clues_iter = SEASON_DETAILS_CONTEXT_CLUES.iter();
@@ -443,6 +455,7 @@ mod tests {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::default().into());
// This should panic because the route is not a Sonarr route
SonarrContextClueProvider::get_context_clues(&mut app);
}
@@ -464,16 +477,6 @@ mod tests {
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[test]
fn test_sonarr_context_clue_provider_series_overview_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::SeriesOverview.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &SERIES_OVERVIEW_CONTEXT_CLUES);
}
#[rstest]
#[case(0, ActiveSonarrBlock::SeasonDetails, &SEASON_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveSonarrBlock::SeasonHistory, &SEASON_HISTORY_CONTEXT_CLUES)]
@@ -635,24 +638,6 @@ mod tests {
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_delete_series_blocks(
#[values(
ActiveSonarrBlock::DeleteSeriesPrompt,
ActiveSonarrBlock::DeleteSeriesConfirmPrompt,
ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion,
ActiveSonarrBlock::DeleteSeriesToggleDeleteFile
)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_sonarr_context_clue_provider_add_series_search_results_clues(
#[values(
+2 -2
View File
@@ -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);
-88
View File
@@ -8,28 +8,16 @@ mod tests {
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::readarr::ReadarrCommand;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::readarr_network::ReadarrEvent;
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,
},
readarr_models::{
BlocklistItem as ReadarrBlocklistItem, BlocklistResponse as ReadarrBlocklistResponse,
ReadarrSerdeable,
},
sonarr_models::{
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
SonarrSerdeable,
@@ -67,20 +55,6 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_lidarr_subcommand_delegates_to_lidarr() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
assert_ok!(&result);
}
#[test]
fn test_readarr_subcommand_delegates_to_readarr() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "authors"]);
assert_ok!(&result);
}
#[test]
fn test_completions_requires_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
@@ -200,66 +174,4 @@ mod tests {
assert_ok!(&result);
}
#[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);
}
#[tokio::test]
async fn test_cli_handler_delegates_readarr_commands_to_the_readarr_cli_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::BlocklistResponse(
ReadarrBlocklistResponse {
records: vec![ReadarrBlocklistItem::default()],
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::ClearBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let clear_blocklist_command = ReadarrCommand::ClearBlocklist.into();
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
assert_ok!(&result);
}
}
-223
View File
@@ -1,223 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, Subcommand};
use tokio::sync::Mutex;
use super::LidarrCommand;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::lidarr_models::{
AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, MonitorType, NewItemMonitorType,
},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
#[cfg(test)]
#[path = "add_command_handler_tests.rs"]
mod add_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrAddCommand {
#[command(about = "Add a new artist to your Lidarr library")]
Artist {
#[arg(
long,
help = "The MusicBrainz foreign artist ID of the artist you wish to add to your library",
required = true
)]
foreign_artist_id: String,
#[arg(long, help = "The name of the artist", required = true)]
artist_name: String,
#[arg(
long,
help = "The root folder path where all artist data and metadata should live",
required = true
)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the quality profile to use for this artist",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the metadata profile to use for this artist",
required = true
)]
metadata_profile_id: i64,
#[arg(long, help = "Disable monitoring for this artist")]
disable_monitoring: bool,
#[arg(
long,
help = "Tag IDs to tag the artist with",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
#[arg(
long,
help = "What Lidarr should monitor for this artist",
value_enum,
default_value_t = MonitorType::default()
)]
monitor: MonitorType,
#[arg(
long,
help = "How Lidarr should monitor new items for this artist",
value_enum,
default_value_t = NewItemMonitorType::default()
)]
monitor_new_items: NewItemMonitorType,
#[arg(
long,
help = "Tell Lidarr to not start a search for missing albums once the artist is added to your library"
)]
no_search_for_missing_albums: bool,
},
#[command(about = "Add a new root folder")]
RootFolder {
#[arg(long, help = "The name of the root folder", required = true)]
name: String,
#[arg(long, help = "The path of the new root folder", required = true)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the default quality profile for artists in this root folder",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the default metadata profile for artists in this root folder",
required = true
)]
metadata_profile_id: i64,
#[arg(
long,
help = "The default monitor option for artists in this root folder",
value_enum,
default_value_t = MonitorType::default()
)]
monitor: MonitorType,
#[arg(
long,
help = "The default monitor new items option for artists in this root folder",
value_enum,
default_value_t = NewItemMonitorType::default()
)]
monitor_new_items: NewItemMonitorType,
#[arg(
long,
help = "Default tag IDs for artists in this root folder",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
},
#[command(about = "Add new tag")]
Tag {
#[arg(long, help = "The name of the tag to be added", required = true)]
name: String,
},
}
impl From<LidarrAddCommand> for Command {
fn from(value: LidarrAddCommand) -> Self {
Command::Lidarr(LidarrCommand::Add(value))
}
}
pub(super) struct LidarrAddCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrAddCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrAddCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrAddCommandHandler {
_app: app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrAddCommand::Artist {
foreign_artist_id,
artist_name,
root_folder_path,
quality_profile_id,
metadata_profile_id,
disable_monitoring,
tag: tags,
monitor,
monitor_new_items,
no_search_for_missing_albums,
} => {
let body = AddArtistBody {
foreign_artist_id,
artist_name,
monitored: !disable_monitoring,
root_folder_path,
quality_profile_id,
metadata_profile_id,
tags,
tag_input_string: None,
add_options: AddArtistOptions {
monitor,
monitor_new_items,
search_for_missing_albums: !no_search_for_missing_albums,
},
};
let resp = self
.network
.handle_network_event(LidarrEvent::AddArtist(body).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrAddCommand::RootFolder {
name,
root_folder_path,
quality_profile_id,
metadata_profile_id,
monitor,
monitor_new_items,
tag: tags,
} => {
let add_root_folder_body = AddLidarrRootFolderBody {
name,
path: root_folder_path,
default_quality_profile_id: quality_profile_id,
default_metadata_profile_id: metadata_profile_id,
default_monitor_option: monitor,
default_new_item_monitor_option: monitor_new_items,
default_tags: tags,
tag_input_string: None,
};
let resp = self
.network
.handle_network_event(LidarrEvent::AddRootFolder(add_root_folder_body).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrAddCommand::Tag { name } => {
let resp = self
.network
.handle_network_event(LidarrEvent::AddTag(name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
-561
View File
@@ -1,561 +0,0 @@
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser, error::ErrorKind};
use crate::{
Cli,
cli::{
Command,
lidarr::{LidarrCommand, add_command_handler::LidarrAddCommand},
},
models::lidarr_models::{MonitorType, NewItemMonitorType},
};
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_add_command_from() {
let command = LidarrAddCommand::Tag {
name: String::new(),
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Add(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_add_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "root-folder"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_root_folder_success() {
let expected_args = LidarrAddCommand::RootFolder {
name: "Music".to_owned(),
root_folder_path: "/nfs/test".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
tag: vec![],
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"root-folder",
"--name",
"Music",
"--root-folder-path",
"/nfs/test",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "tag"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_tag_success() {
let expected_args = LidarrAddCommand::Tag {
name: "test".to_owned(),
};
let result = Cli::try_parse_from(["managarr", "lidarr", "add", "tag", "--name", "test"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_foreign_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_artist_name() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_root_folder_path() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_quality_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_requires_metadata_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_artist_success_with_required_args_only() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
disable_monitoring: false,
tag: vec![],
monitor: MonitorType::default(),
monitor_new_items: NewItemMonitorType::default(),
no_search_for_missing_albums: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_success_with_all_args() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 2,
disable_monitoring: true,
tag: vec![1, 2],
monitor: MonitorType::Future,
monitor_new_items: NewItemMonitorType::New,
no_search_for_missing_albums: true,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--disable-monitoring",
"--tag",
"1",
"--tag",
"2",
"--monitor",
"future",
"--monitor-new-items",
"new",
"--no-search-for-missing-albums",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_artist_monitor_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--monitor",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_artist_new_item_monitor_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--monitor-new-items",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_artist_tags_is_repeatable() {
let expected_args = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 2,
disable_monitoring: false,
tag: vec![1, 2],
monitor: MonitorType::default(),
monitor_new_items: NewItemMonitorType::default(),
no_search_for_missing_albums: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"artist",
"--foreign-artist-id",
"test-id",
"--artist-name",
"Test Artist",
"--root-folder-path",
"/music",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
use crate::models::Serdeable;
use crate::models::lidarr_models::{
AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, LidarrSerdeable, MonitorType,
NewItemMonitorType,
};
use crate::network::lidarr_network::LidarrEvent;
use crate::{
app::App,
network::{MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_add_root_folder_command() {
let expected_root_folder_path = "/nfs/test".to_owned();
let expected_add_root_folder_body = AddLidarrRootFolderBody {
name: "Music".to_owned(),
path: expected_root_folder_path.clone(),
default_quality_profile_id: 1,
default_metadata_profile_id: 1,
default_monitor_option: MonitorType::All,
default_new_item_monitor_option: NewItemMonitorType::All,
default_tags: vec![1, 2],
tag_input_string: None,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_root_folder_command = LidarrAddCommand::RootFolder {
name: "Music".to_owned(),
root_folder_path: expected_root_folder_path,
quality_profile_id: 1,
metadata_profile_id: 1,
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
tag: vec![1, 2],
};
let result =
LidarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_add_tag_command() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = LidarrAddCommand::Tag {
name: expected_tag_name,
};
let result = LidarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_add_artist_command() {
let expected_body = AddArtistBody {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
monitored: false,
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddArtistOptions {
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
search_for_missing_albums: false,
},
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddArtist(expected_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_artist_command = LidarrAddCommand::Artist {
foreign_artist_id: "test-id".to_owned(),
artist_name: "Test Artist".to_owned(),
root_folder_path: "/music".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
disable_monitoring: true,
tag: vec![1, 2],
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
no_search_for_missing_albums: true,
};
let result = LidarrAddCommandHandler::with(&app_arc, add_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
-181
View File
@@ -1,181 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::lidarr_models::DeleteParams,
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "delete_command_handler_tests.rs"]
mod delete_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrDeleteCommand {
#[command(about = "Delete an album from your Lidarr library")]
Album {
#[arg(long, help = "The ID of the album to delete", required = true)]
album_id: i64,
#[arg(long, help = "Delete the album files from disk as well")]
delete_files_from_disk: bool,
#[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)]
track_file_id: i64,
},
#[command(about = "Delete an artist from your Lidarr library")]
Artist {
#[arg(long, help = "The ID of the artist to delete", required = true)]
artist_id: i64,
#[arg(long, help = "Delete the artist files from disk as well")]
delete_files_from_disk: bool,
#[arg(long, help = "Add a list exclusion for this artist")]
add_list_exclusion: bool,
},
#[command(about = "Delete the specified download")]
Download {
#[arg(long, help = "The ID of the download to delete", required = true)]
download_id: i64,
},
#[command(about = "Delete the indexer with the given ID")]
Indexer {
#[arg(long, help = "The ID of the indexer to delete", required = true)]
indexer_id: i64,
},
#[command(about = "Delete the root folder with the given ID")]
RootFolder {
#[arg(long, help = "The ID of the root folder to delete", required = true)]
root_folder_id: i64,
},
#[command(about = "Delete the tag with the specified ID")]
Tag {
#[arg(long, help = "The ID of the tag to delete", required = true)]
tag_id: i64,
},
}
impl From<LidarrDeleteCommand> for Command {
fn from(value: LidarrDeleteCommand) -> Self {
Command::Lidarr(LidarrCommand::Delete(value))
}
}
pub(super) struct LidarrDeleteCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrDeleteCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrDeleteCommand::Album {
album_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_album_params = DeleteParams {
id: album_id,
delete_files: delete_files_from_disk,
add_import_list_exclusion: add_list_exclusion,
};
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteAlbum(delete_album_params).into())
.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
.handle_network_event(LidarrEvent::DeleteTrackFile(track_file_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Artist {
artist_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_artist_params = DeleteParams {
id: artist_id,
delete_files: delete_files_from_disk,
add_import_list_exclusion: add_list_exclusion,
};
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteArtist(delete_artist_params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Download { download_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteDownload(download_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Indexer { indexer_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteIndexer(indexer_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::RootFolder { root_folder_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteRootFolder(root_folder_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Tag { tag_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteTag(tag_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,595 +0,0 @@
#[cfg(test)]
mod tests {
use crate::{
Cli,
cli::{
Command,
lidarr::{LidarrCommand, delete_command_handler::LidarrDeleteCommand},
},
};
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_delete_command_from() {
let command = LidarrDeleteCommand::Artist {
artist_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Delete(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_delete_album_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "album"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_album_defaults() {
let expected_args = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result =
Cli::try_parse_from(["managarr", "lidarr", "delete", "album", "--album-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_album_all_args_defined() {
let expected_args = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"album",
"--album-id",
"1",
"--delete-files-from-disk",
"--add-list-exclusion",
]);
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_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 =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "track-file"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_track_file_success() {
let expected_args = LidarrDeleteCommand::TrackFile { track_file_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"track-file",
"--track-file-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_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_artist_defaults() {
let expected_args = LidarrDeleteCommand::Artist {
artist_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result =
Cli::try_parse_from(["managarr", "lidarr", "delete", "artist", "--artist-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_artist_all_args_defined() {
let expected_args = LidarrDeleteCommand::Artist {
artist_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"artist",
"--artist-id",
"1",
"--delete-files-from-disk",
"--add-list-exclusion",
]);
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_download_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "download"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_download_success() {
let expected_args = LidarrDeleteCommand::Download { download_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"download",
"--download-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_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "indexer"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_indexer_success() {
let expected_args = LidarrDeleteCommand::Indexer { indexer_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"indexer",
"--indexer-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_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "root-folder"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_root_folder_success() {
let expected_args = LidarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"root-folder",
"--root-folder-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_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_tag_success() {
let expected_args = LidarrDeleteCommand::Tag { tag_id: 1 };
let result = Cli::try_parse_from(["managarr", "lidarr", "delete", "tag", "--tag-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);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
CliCommandHandler,
lidarr::delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler},
},
models::{
Serdeable,
lidarr_models::{DeleteParams, LidarrSerdeable},
},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_delete_album_command() {
let expected_delete_album_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteAlbum(expected_delete_album_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_album_command = LidarrDeleteCommand::Album {
album_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_album_command, &mut mock_network)
.handle()
.await;
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;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteTrackFile(expected_track_file_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_track_file_command = LidarrDeleteCommand::TrackFile { track_file_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_track_file_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_artist_command() {
let expected_delete_artist_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteArtist(expected_delete_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_artist_command = LidarrDeleteCommand::Artist {
artist_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_download_command() {
let expected_download_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteDownload(expected_download_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_download_command = LidarrDeleteCommand::Download { download_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_indexer_command() {
let expected_indexer_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteIndexer(expected_indexer_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_indexer_command = LidarrDeleteCommand::Indexer { indexer_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_root_folder_command() {
let expected_root_folder_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteRootFolder(expected_root_folder_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_root_folder_command = LidarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_tag_command() {
let expected_tag_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteTag(expected_tag_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_tag_command = LidarrDeleteCommand::Tag { tag_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
-343
View File
@@ -1,343 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, ArgGroup, Subcommand};
use tokio::sync::Mutex;
use super::LidarrCommand;
use crate::models::Serdeable;
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
use crate::{
app::App,
cli::{CliCommandHandler, Command, mutex_flags_or_option},
models::lidarr_models::{EditArtistParams, NewItemMonitorType},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
#[cfg(test)]
#[path = "edit_command_handler_tests.rs"]
mod edit_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrEditCommand {
#[command(
about = "Edit and indexer settings that apply to all indexers",
group(
ArgGroup::new("edit_settings")
.args([
"maximum_size",
"minimum_age",
"retention",
"rss_sync_interval",
]).required(true)
.multiple(true))
)]
AllIndexerSettings {
#[arg(
long,
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
)]
maximum_size: Option<i64>,
#[arg(
long,
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
)]
minimum_age: Option<i64>,
#[arg(
long,
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
)]
retention: Option<i64>,
#[arg(
long,
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
)]
rss_sync_interval: Option<i64>,
},
#[command(
about = "Edit preferences for the specified artist",
group(
ArgGroup::new("edit_artist")
.args([
"enable_monitoring",
"disable_monitoring",
"monitor_new_items",
"quality_profile_id",
"metadata_profile_id",
"root_folder_path",
"tag",
"clear_tags"
]).required(true)
.multiple(true))
)]
Artist {
#[arg(
long,
help = "The ID of the artist whose settings you want to edit",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "Enable monitoring of this artist in Lidarr so Lidarr will automatically download releases from this artist if they are available",
conflicts_with = "disable_monitoring"
)]
enable_monitoring: bool,
#[arg(
long,
help = "Disable monitoring of this artist so Lidarr does not automatically download releases from this artist if they are available",
conflicts_with = "enable_monitoring"
)]
disable_monitoring: bool,
#[arg(
long,
help = "How Lidarr should monitor new albums from this artist",
value_enum
)]
monitor_new_items: Option<NewItemMonitorType>,
#[arg(long, help = "The ID of the quality profile to use for this artist")]
quality_profile_id: Option<i64>,
#[arg(long, help = "The ID of the metadata profile to use for this artist")]
metadata_profile_id: Option<i64>,
#[arg(
long,
help = "The root folder path where all artist data and metadata should live"
)]
root_folder_path: Option<String>,
#[arg(
long,
help = "Tag IDs to tag this artist with",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(long, help = "Clear all tags on this artist", conflicts_with = "tag")]
clear_tags: bool,
},
#[command(
about = "Edit preferences for the specified indexer",
group(
ArgGroup::new("edit_indexer")
.args([
"name",
"enable_rss",
"disable_rss",
"enable_automatic_search",
"disable_automatic_search",
"enable_interactive_search",
"disable_automatic_search",
"url",
"api_key",
"seed_ratio",
"tag",
"priority",
"clear_tags"
]).required(true)
.multiple(true))
)]
Indexer {
#[arg(
long,
help = "The ID of the indexer whose settings you wish to edit",
required = true
)]
indexer_id: i64,
#[arg(long, help = "The name of the indexer")]
name: Option<String>,
#[arg(
long,
help = "Indicate to Lidarr that this indexer should be used when Lidarr periodically looks for releases via RSS Sync",
conflicts_with = "disable_rss"
)]
enable_rss: bool,
#[arg(
long,
help = "Disable using this indexer when Lidarr periodically looks for releases via RSS Sync",
conflicts_with = "enable_rss"
)]
disable_rss: bool,
#[arg(
long,
help = "Indicate to Lidarr that this indexer should be used when automatic searches are performed via the UI or by Lidarr",
conflicts_with = "disable_automatic_search"
)]
enable_automatic_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Lidarr",
conflicts_with = "enable_automatic_search"
)]
disable_automatic_search: bool,
#[arg(
long,
help = "Indicate to Lidarr that this indexer should be used when an interactive search is used",
conflicts_with = "disable_interactive_search"
)]
enable_interactive_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever an interactive search is performed",
conflicts_with = "enable_interactive_search"
)]
disable_interactive_search: bool,
#[arg(long, help = "The URL of the indexer")]
url: Option<String>,
#[arg(long, help = "The API key used to access the indexer's API")]
api_key: Option<String>,
#[arg(
long,
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
)]
seed_ratio: Option<String>,
#[arg(
long,
help = "Only use this indexer for series with at least one matching tag ID. Leave blank to use with all series.",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(
long,
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Lidarr will still use all enabled indexers for RSS Sync and Searching"
)]
priority: Option<i64>,
#[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")]
clear_tags: bool,
},
}
impl From<LidarrEditCommand> for Command {
fn from(value: LidarrEditCommand) -> Self {
Command::Lidarr(LidarrCommand::Edit(value))
}
}
pub(super) struct LidarrEditCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrEditCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrEditCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrEditCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrEditCommand::AllIndexerSettings {
maximum_size,
minimum_age,
retention,
rss_sync_interval,
} => {
if let Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(previous_indexer_settings)) = self
.network
.handle_network_event(LidarrEvent::GetAllIndexerSettings.into())
.await?
{
let params = IndexerSettings {
id: 1,
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
retention: retention.unwrap_or(previous_indexer_settings.retention),
rss_sync_interval: rss_sync_interval
.unwrap_or(previous_indexer_settings.rss_sync_interval),
};
self
.network
.handle_network_event(LidarrEvent::EditAllIndexerSettings(params).into())
.await?;
"All indexer settings updated".to_owned()
} else {
String::new()
}
}
LidarrEditCommand::Artist {
artist_id,
enable_monitoring,
disable_monitoring,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tag,
clear_tags,
} => {
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
let edit_artist_params = EditArtistParams {
artist_id,
monitored: monitored_value,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags: tag,
tag_input_string: None,
clear_tags,
};
self
.network
.handle_network_event(LidarrEvent::EditArtist(edit_artist_params).into())
.await?;
"Artist Updated".to_owned()
}
LidarrEditCommand::Indexer {
indexer_id,
name,
enable_rss,
disable_rss,
enable_automatic_search,
disable_automatic_search,
enable_interactive_search,
disable_interactive_search,
url,
api_key,
seed_ratio,
tag,
priority,
clear_tags,
} => {
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
let automatic_search_value =
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
let interactive_search_value =
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
let edit_indexer_params = EditIndexerParams {
indexer_id,
name,
enable_rss: rss_value,
enable_automatic_search: automatic_search_value,
enable_interactive_search: interactive_search_value,
url,
api_key,
seed_ratio,
tags: tag,
tag_input_string: None,
priority,
clear_tags,
};
self
.network
.handle_network_event(LidarrEvent::EditIndexer(edit_indexer_params).into())
.await?;
"Indexer updated".to_owned()
}
};
Ok(result)
}
}
@@ -1,858 +0,0 @@
#[cfg(test)]
mod tests {
use crate::cli::{
Command,
lidarr::{LidarrCommand, edit_command_handler::LidarrEditCommand},
};
#[test]
fn test_lidarr_edit_command_from() {
let command = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: false,
monitor_new_items: None,
quality_profile_id: None,
metadata_profile_id: None,
root_folder_path: None,
tag: None,
clear_tags: false,
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Edit(command)));
}
mod cli {
use crate::{Cli, models::lidarr_models::NewItemMonitorType};
use super::*;
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_edit_all_indexer_settings_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "all-indexer-settings"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_edit_all_indexer_settings_assert_argument_flags_require_args(
#[values(
"--maximum-size",
"--minimum-age",
"--retention",
"--rss-sync-interval"
)]
flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"all-indexer-settings",
flag,
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() {
let expected_args = LidarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: None,
retention: None,
rss_sync_interval: None,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"all-indexer-settings",
"--maximum-size",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_all_indexer_settings_all_arguments_defined() {
let expected_args = LidarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"all-indexer-settings",
"--maximum-size",
"1",
"--minimum-age",
"1",
"--retention",
"1",
"--rss-sync-interval",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_artist_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_artist_with_artist_id_still_requires_arguments() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_artist_monitoring_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--enable-monitoring",
"--disable-monitoring",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_artist_tag_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--tag",
"1",
"--clear-tags",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[rstest]
fn test_edit_artist_assert_argument_flags_require_args(
#[values(
"--monitor-new-items",
"--quality-profile-id",
"--metadata-profile-id",
"--root-folder-path",
"--tag"
)]
flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
flag,
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_artist_monitor_new_items_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--monitor-new-items",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_artist_only_requires_at_least_one_argument_plus_artist_id() {
let expected_args = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: false,
monitor_new_items: None,
quality_profile_id: None,
metadata_profile_id: None,
root_folder_path: Some("/nfs/test".to_owned()),
tag: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--root-folder-path",
"/nfs/test",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_artist_tag_argument_is_repeatable() {
let expected_args = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: false,
monitor_new_items: None,
quality_profile_id: None,
metadata_profile_id: None,
root_folder_path: None,
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_artist_all_arguments_defined() {
let expected_args = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: true,
disable_monitoring: false,
monitor_new_items: Some(NewItemMonitorType::New),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"artist",
"--artist-id",
"1",
"--enable-monitoring",
"--monitor-new-items",
"new",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
"--root-folder-path",
"/nfs/test",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "indexer"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_indexer_with_indexer_id_still_requires_arguments() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_indexer_rss_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-rss",
"--disable-rss",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_automatic_search_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-automatic-search",
"--disable-automatic-search",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_interactive_search_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-interactive-search",
"--disable-interactive-search",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_tag_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--tag",
"1",
"--clear-tags",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[rstest]
fn test_edit_indexer_assert_argument_flags_require_args(
#[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
flag,
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() {
let expected_args = LidarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: false,
disable_rss: false,
enable_automatic_search: false,
disable_automatic_search: false,
enable_interactive_search: false,
disable_interactive_search: false,
url: None,
api_key: None,
seed_ratio: None,
tag: None,
priority: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--name",
"Test",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_indexer_tag_argument_is_repeatable() {
let expected_args = LidarrEditCommand::Indexer {
indexer_id: 1,
name: None,
enable_rss: false,
disable_rss: false,
enable_automatic_search: false,
disable_automatic_search: false,
enable_interactive_search: false,
disable_interactive_search: false,
url: None,
api_key: None,
seed_ratio: None,
tag: Some(vec![1, 2]),
priority: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_indexer_all_arguments_defined() {
let expected_args = LidarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: true,
disable_rss: false,
enable_automatic_search: true,
disable_automatic_search: false,
enable_interactive_search: true,
disable_interactive_search: false,
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tag: Some(vec![1, 2]),
priority: Some(25),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--name",
"Test",
"--enable-rss",
"--enable-automatic-search",
"--enable-interactive-search",
"--url",
"http://test.com",
"--api-key",
"testKey",
"--seed-ratio",
"1.2",
"--tag",
"1",
"--tag",
"2",
"--priority",
"25",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
use crate::{
app::App,
cli::{
CliCommandHandler,
lidarr::edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler},
},
models::{
Serdeable,
lidarr_models::{EditArtistParams, LidarrSerdeable, NewItemMonitorType},
},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_edit_all_indexer_settings_command() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 12345,
minimum_age: 11,
retention: 29,
rss_sync_interval: 60,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(
IndexerSettings {
id: 5,
maximum_size: 22222,
minimum_age: 34,
retention: 48,
rss_sync_interval: 90,
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = LidarrEditCommand::AllIndexerSettings {
maximum_size: Some(12345),
minimum_age: Some(11),
retention: Some(29),
rss_sync_interval: Some(60),
};
let result = LidarrEditCommandHandler::with(
&app_arc,
edit_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_edit_artist_command() {
let expected_edit_artist_params = EditArtistParams {
artist_id: 1,
monitored: Some(true),
monitor_new_items: Some(NewItemMonitorType::New),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_artist_command = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: true,
disable_monitoring: false,
monitor_new_items: Some(NewItemMonitorType::New),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_edit_artist_command_handles_disable_monitoring_flag_properly() {
let expected_edit_artist_params = EditArtistParams {
artist_id: 1,
monitored: Some(false),
monitor_new_items: Some(NewItemMonitorType::None),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_artist_command = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: true,
monitor_new_items: Some(NewItemMonitorType::None),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_edit_artist_command_no_monitoring_boolean_flags_returns_none_value() {
let expected_edit_artist_params = EditArtistParams {
artist_id: 1,
monitored: None,
monitor_new_items: Some(NewItemMonitorType::All),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_artist_command = LidarrEditCommand::Artist {
artist_id: 1,
enable_monitoring: false,
disable_monitoring: false,
monitor_new_items: Some(NewItemMonitorType::All),
quality_profile_id: Some(1),
metadata_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_edit_indexer_command() {
let expected_edit_indexer_params = EditIndexerParams {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: Some(true),
enable_automatic_search: Some(true),
enable_interactive_search: Some(true),
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tags: Some(vec![1, 2]),
tag_input_string: None,
priority: Some(25),
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditIndexer(expected_edit_indexer_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_indexer_command = LidarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: true,
disable_rss: false,
enable_automatic_search: true,
disable_automatic_search: false,
enable_interactive_search: true,
disable_interactive_search: false,
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tag: Some(vec![1, 2]),
priority: Some(25),
clear_tags: false,
};
let result =
LidarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
-138
View File
@@ -1,138 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "get_command_handler_tests.rs"]
mod get_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrGetCommand {
#[command(about = "Get detailed information for the album with the given ID")]
AlbumDetails {
#[arg(
long,
help = "The Lidarr ID of the album whose details you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "Get the shared settings for all indexers")]
AllIndexerSettings,
#[command(about = "Get detailed information for the artist with the given ID")]
ArtistDetails {
#[arg(
long,
help = "The Lidarr ID of the artist whose details you wish to fetch",
required = true
)]
artist_id: i64,
},
#[command(about = "Fetch the host config for your Lidarr instance")]
HostConfig,
#[command(about = "Fetch the security config for your Lidarr instance")]
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 {
fn from(value: LidarrGetCommand) -> Self {
Command::Lidarr(LidarrCommand::Get(value))
}
}
pub(super) struct LidarrGetCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrGetCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrGetCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrGetCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrGetCommand::AlbumDetails { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbumDetails(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::AllIndexerSettings => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAllIndexerSettings.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::ArtistDetails { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetArtistDetails(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::HostConfig => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetHostConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::SecurityConfig => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetSecurityConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrGetCommand::SystemStatus => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetStatus.into())
.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)
}
}
-329
View File
@@ -1,329 +0,0 @@
#[cfg(test)]
mod tests {
use crate::Cli;
use crate::cli::{
Command,
lidarr::{LidarrCommand, get_command_handler::LidarrGetCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_get_command_from() {
let command = LidarrGetCommand::SystemStatus;
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Get(command)));
}
mod cli {
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_album_details_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "album-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"get",
"album-details",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_all_indexer_settings_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "all-indexer-settings"]);
assert_ok!(&result);
}
#[test]
fn test_artist_details_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "artist-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_artist_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"get",
"artist-details",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_host_config_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "host-config"]);
assert_ok!(&result);
}
#[test]
fn test_security_config_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "security-config"]);
assert_ok!(&result);
}
#[test]
fn test_system_status_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "system-status"]);
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 {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
CliCommandHandler,
lidarr::get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler},
},
models::{Serdeable, lidarr_models::LidarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_get_album_details_command() {
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumDetails(expected_album_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_album_details_command = LidarrGetCommand::AlbumDetails { album_id: 1 };
let result =
LidarrGetCommandHandler::with(&app_arc, get_album_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_all_indexer_settings_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_all_indexer_settings_command = LidarrGetCommand::AllIndexerSettings;
let result = LidarrGetCommandHandler::with(
&app_arc,
get_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_artist_details_command() {
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetArtistDetails(expected_artist_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_artist_details_command = LidarrGetCommand::ArtistDetails { artist_id: 1 };
let result =
LidarrGetCommandHandler::with(&app_arc, get_artist_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_host_config_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetHostConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_host_config_command = LidarrGetCommand::HostConfig;
let result =
LidarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_security_config_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetSecurityConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_security_config_command = LidarrGetCommand::SecurityConfig;
let result =
LidarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_system_status_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = LidarrGetCommand::SystemStatus;
let result =
LidarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
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);
}
}
}
-773
View File
@@ -1,773 +0,0 @@
#[cfg(test)]
mod tests {
use crate::Cli;
use crate::cli::{
Command,
lidarr::{LidarrCommand, list_command_handler::LidarrListCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_command_from() {
let command = LidarrCommand::List(LidarrListCommand::Artists);
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(command));
}
mod cli {
use super::*;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_commands_that_have_no_arg_requirements(
#[values("clear-blocklist", "test-all-indexers")] subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", subcommand]);
assert_ok!(&result);
}
#[test]
fn test_list_artists_has_no_arg_requirements() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
assert_ok!(&result);
}
#[test]
fn test_lidarr_list_subcommand_requires_subcommand() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list"]);
assert_err!(&result);
}
#[test]
fn test_lidarr_add_subcommand_requires_subcommand() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add"]);
assert_err!(&result);
}
#[test]
fn test_lidarr_delete_subcommand_requires_subcommand() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete"]);
assert_err!(&result);
}
#[test]
fn test_download_release_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--indexer-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--guid",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"download-release",
"--guid",
"1",
"--indexer-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_toggle_artist_monitoring_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-artist-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_artist_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"toggle-artist-monitoring",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_toggle_album_monitoring_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-album-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_album_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"toggle-album-monitoring",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_search_new_artist_requires_query() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "search-new-artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_search_new_artist_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"search-new-artist",
"--query",
"test query",
]);
assert_ok!(&result);
}
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "start-task"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_start_task_task_name_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"start-task",
"--task-name",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_start_task_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"start-task",
"--task-name",
"application-update-check",
]);
assert_ok!(&result);
}
#[test]
fn test_mark_history_item_as_failed_requires_history_item_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "mark-history-item-as-failed"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_mark_history_item_as_failed_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"mark-history-item-as-failed",
"--history-item-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_test_indexer_requires_indexer_id() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "test-indexer"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_test_indexer_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"test-indexer",
"--indexer-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::lidarr::add_command_handler::LidarrAddCommand;
use crate::cli::lidarr::edit_command_handler::LidarrEditCommand;
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
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::{BlocklistItem, BlocklistResponse, LidarrTaskName};
use crate::models::servarr_models::{IndexerSettings, ReleaseDownloadBody};
use crate::{
app::App,
cli::{
CliCommandHandler,
lidarr::{
LidarrCliHandler, LidarrCommand, delete_command_handler::LidarrDeleteCommand,
list_command_handler::LidarrListCommand,
},
},
models::{
Serdeable,
lidarr_models::{Artist, DeleteParams, LidarrSerdeable},
},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = LidarrCommand::Add(LidarrAddCommand::Tag {
name: expected_tag_name,
});
let result = LidarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = LidarrCommand::Get(LidarrGetCommand::SystemStatus);
let result = LidarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
let expected_delete_artist_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: true,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteArtist(expected_delete_artist_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_artist_command = LidarrCommand::Delete(LidarrDeleteCommand::Artist {
artist_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
});
let result = LidarrCliHandler::with(&app_arc, delete_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 1,
minimum_age: 1,
retention: 1,
rss_sync_interval: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(
IndexerSettings {
id: 1,
maximum_size: 2,
minimum_age: 2,
retention: 2,
rss_sync_interval: 2,
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command =
LidarrCommand::Edit(LidarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
});
let result = LidarrCliHandler::with(
&app_arc,
edit_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::ListArtists.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Artists(vec![
Artist::default(),
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_artists_command = LidarrCommand::List(LidarrListCommand::Artists);
let result = LidarrCliHandler::with(&app_arc, list_artists_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::UpdateAllArtists.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_artist_command = LidarrCommand::Refresh(LidarrRefreshCommand::AllArtists);
let result = LidarrCliHandler::with(&app_arc, refresh_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler()
{
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command =
LidarrCommand::ManualSearch(LidarrManualSearchCommand::Discography { artist_id: 1 });
let result =
LidarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist {
artist_id: 1,
});
let result = LidarrCliHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
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 = ReleaseDownloadBody {
guid: "guid".to_owned(),
indexer_id: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DownloadRelease(expected_release_download_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_release_command = LidarrCommand::DownloadRelease {
guid: "guid".to_owned(),
indexer_id: 1,
};
let result = LidarrCliHandler::with(&app_arc, download_release_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_toggle_artist_monitoring_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::ToggleArtistMonitoring(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_artist_monitoring_command = LidarrCommand::ToggleArtistMonitoring { artist_id: 1 };
let result = LidarrCliHandler::with(
&app_arc,
toggle_artist_monitoring_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_search_new_artist_command() {
let expected_query = "test artist".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::SearchNewArtist(expected_query.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let search_new_artist_command = LidarrCommand::SearchNewArtist {
query: expected_query,
};
let result = LidarrCliHandler::with(&app_arc, search_new_artist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_start_task_command() {
let expected_task_name = LidarrTaskName::ApplicationUpdateCheck;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::StartTask(expected_task_name).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let start_task_command = LidarrCommand::StartTask {
task_name: LidarrTaskName::ApplicationUpdateCheck,
};
let result = LidarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_test_indexer_command() {
let expected_indexer_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TestIndexer(expected_indexer_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_indexer_command = LidarrCommand::TestIndexer { indexer_id: 1 };
let result = LidarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_test_all_indexers_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::TestAllIndexers.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_all_indexers_command = LidarrCommand::TestAllIndexers;
let result = LidarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_mark_history_item_as_failed_command() {
let expected_history_item_id = 1i64;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::MarkHistoryItemAsFailed(expected_history_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 mark_history_item_as_failed_command = LidarrCommand::MarkHistoryItemAsFailed {
history_item_id: expected_history_item_id,
};
let result = LidarrCliHandler::with(
&app_arc,
mark_history_item_as_failed_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
-350
View File
@@ -1,350 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
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},
};
#[cfg(test)]
#[path = "list_command_handler_tests.rs"]
mod list_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrListCommand {
#[command(about = "List all albums for the artist with the given ID")]
Albums {
#[arg(
long,
help = "The Lidarr ID of the artist whose albums you want to list",
required = true
)]
artist_id: i64,
},
#[command(
about = "Fetch all history events for the given album corresponding to the artist with the given ID."
)]
AlbumHistory {
#[arg(
long,
help = "The Lidarr artist ID of the artist whose history you wish to fetch and list",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The Lidarr album ID to fetch history events for",
required = true
)]
album_id: i64,
},
#[command(about = "Fetch all history events for the artist with the given ID")]
ArtistHistory {
#[arg(
long,
help = "The Lidarr ID of the artist whose history you wish to fetch",
required = true
)]
artist_id: i64,
},
#[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)]
count: u64,
},
#[command(about = "Fetch all Lidarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
events: u64,
},
#[command(about = "List all Lidarr indexers")]
Indexers,
#[command(about = "Fetch Lidarr logs")]
Logs {
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
events: u64,
#[arg(
long,
help = "Output the logs in the same format as they appear in the log files"
)]
output_in_log_format: bool,
},
#[command(about = "List all Lidarr metadata profiles")]
MetadataProfiles,
#[command(about = "List all Lidarr quality profiles")]
QualityProfiles,
#[command(about = "List all queued events")]
QueuedEvents,
#[command(about = "List all root folders in Lidarr")]
RootFolders,
#[command(about = "List all Lidarr tags")]
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"
)]
Tracks {
#[arg(
long,
help = "The Lidarr artist ID of the artist whose tracks you wish to fetch",
required = true
)]
artist_id: i64,
#[arg(
long,
help = "The Lidarr album ID whose tracks you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "List the track files for the album with the given ID")]
TrackFiles {
#[arg(
long,
help = "The Lidarr ID of the album whose track files you wish to fetch",
required = true
)]
album_id: i64,
},
#[command(about = "List all Lidarr updates")]
Updates,
}
impl From<LidarrListCommand> for Command {
fn from(value: LidarrListCommand) -> Self {
Command::Lidarr(LidarrCommand::List(value))
}
}
pub(super) struct LidarrListCommandHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrListCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrListCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrListCommandHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrListCommand::Albums { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbums(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::AlbumHistory {
artist_id,
album_id,
} => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetAlbumHistory(artist_id, album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::ArtistHistory { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetArtistHistory(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Artists => {
let resp = self
.network
.handle_network_event(LidarrEvent::ListArtists.into())
.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
.handle_network_event(LidarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::History { events: items } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetHistory(items).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Indexers => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Logs {
events,
output_in_log_format,
} => {
let logs = self
.network
.handle_network_event(LidarrEvent::GetLogs(events).into())
.await?;
if output_in_log_format {
let log_lines = &self.app.lock().await.data.sonarr_data.logs.items;
serde_json::to_string_pretty(log_lines)?
} else {
serde_json::to_string_pretty(&logs)?
}
}
LidarrListCommand::MetadataProfiles => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetMetadataProfiles.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::QualityProfiles => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetQualityProfiles.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::QueuedEvents => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetQueuedEvents.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::RootFolders => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetRootFolders.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Tags => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTags.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Tasks => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTasks.into())
.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,
} => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTracks(artist_id, album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::TrackFiles { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetTrackFiles(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Updates => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetUpdates.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,728 +0,0 @@
#[cfg(test)]
mod tests {
use crate::Cli;
use crate::cli::{
Command,
lidarr::{LidarrCommand, list_command_handler::LidarrListCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_list_command_from() {
let command = LidarrListCommand::Artists;
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::List(command)));
}
mod cli {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_list_commands_have_no_arg_requirements(
#[values(
"artists",
"blocklist",
"disk-space",
"indexers",
"metadata-profiles",
"quality-profiles",
"queued-events",
"tags",
"tasks",
"updates",
"root-folders"
)]
subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", subcommand]);
assert_ok!(&result);
}
#[test]
fn test_list_albums_requires_artist_id() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "albums"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_albums_with_artist_id() {
let expected_args = LidarrListCommand::Albums { artist_id: 1 };
let result =
Cli::try_parse_from(["managarr", "lidarr", "list", "albums", "--artist-id", "1"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(album_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(album_command, expected_args);
}
#[test]
fn test_album_history_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_history_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_album_history_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"album-history",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_list_artist_history_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artist-history"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_artist_history_success() {
let expected_args = LidarrListCommand::ArtistHistory { artist_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"artist-history",
"--artist-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(artist_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(artist_command, expected_args);
}
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "downloads", "--count"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_downloads_default_values() {
let expected_args = LidarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "downloads"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(downloads_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(downloads_command, expected_args);
}
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "history", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_history_default_values() {
let expected_args = LidarrListCommand::History { events: 500 };
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "history"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(history_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(history_command, expected_args);
}
#[test]
fn test_list_logs_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "logs", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_logs_default_values() {
let expected_args = LidarrListCommand::Logs {
events: 500,
output_in_log_format: false,
};
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "logs"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(logs_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
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([
"managarr",
"lidarr",
"list",
"tracks",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_tracks_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"list",
"tracks",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_tracks_success() {
let expected_args = LidarrListCommand::Tracks {
artist_id: 1,
album_id: 1,
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"tracks",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(tracks_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(tracks_command, expected_args);
}
#[test]
fn test_list_track_files_requires_album_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "track-files"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_track_files_success() {
let expected_args = LidarrListCommand::TrackFiles { album_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"list",
"track-files",
"--album-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(track_files_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(track_files_command, expected_args);
}
}
mod handler {
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;
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
use crate::models::Serdeable;
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},
};
#[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)]
#[case(LidarrListCommand::QueuedEvents, LidarrEvent::GetQueuedEvents)]
#[case(LidarrListCommand::RootFolders, LidarrEvent::GetRootFolders)]
#[case(LidarrListCommand::Tags, LidarrEvent::GetTags)]
#[case(LidarrListCommand::Tasks, LidarrEvent::GetTasks)]
#[case(LidarrListCommand::Updates, LidarrEvent::GetUpdates)]
#[tokio::test]
async fn test_handle_list_command(
#[case] list_command: LidarrListCommand,
#[case] expected_lidarr_event: LidarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(expected_lidarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_albums_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::GetAlbums(1).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_command = LidarrListCommand::Albums { artist_id: 1 };
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_list_album_history_command() {
let expected_artist_id = 1;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumHistory(expected_artist_id, expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_album_history_command = LidarrListCommand::AlbumHistory {
artist_id: 1,
album_id: 1,
};
let result =
LidarrListCommandHandler::with(&app_arc, list_album_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_artist_history_command() {
let expected_artist_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetArtistHistory(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_artist_history_command = LidarrListCommand::ArtistHistory { artist_id: 1 };
let result =
LidarrListCommandHandler::with(&app_arc, list_artist_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_downloads_command() {
let expected_count = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = LidarrListCommand::Downloads { count: 1000 };
let result =
LidarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_history_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetHistory(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_history_command = LidarrListCommand::History { events: 1000 };
let result =
LidarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_logs_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetLogs(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_logs_command = LidarrListCommand::Logs {
events: 1000,
output_in_log_format: false,
};
let result = LidarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
.handle()
.await;
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 = 5;
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;
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTracks(expected_artist_id, expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_tracks_command = LidarrListCommand::Tracks {
artist_id: 1,
album_id: 1,
};
let result = LidarrListCommandHandler::with(&app_arc, list_tracks_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_track_files_command() {
let expected_album_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetTrackFiles(expected_album_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_track_files_command = LidarrListCommand::TrackFiles { album_id: 1 };
let result =
LidarrListCommandHandler::with(&app_arc, list_track_files_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -1,118 +0,0 @@
use crate::app::App;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::{CliCommandHandler, Command};
use crate::models::Serdeable;
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
use crate::network::NetworkTrait;
use crate::network::lidarr_network::LidarrEvent;
use anyhow::Result;
use clap::Subcommand;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::Mutex;
#[cfg(test)]
#[path = "manual_search_command_handler_tests.rs"]
mod manual_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrManualSearchCommand {
#[command(
about = "Trigger a manual search of releases for the given album corresponding to the artist with the given ID"
)]
Album {
#[arg(
long,
help = "The Lidarr ID of the artist whose releases you wish to fetch and list",
required = true
)]
artist_id: i64,
#[arg(long, help = "The Lidarr album ID to search for", required = true)]
album_id: i64,
},
#[command(
about = "Trigger a manual search of discography releases for the given artist corresponding to the artist with the given ID."
)]
Discography {
#[arg(
long,
help = "The Lidarr ID of the artist whose discography releases you wish to fetch and list",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrManualSearchCommand> for Command {
fn from(value: LidarrManualSearchCommand) -> Self {
Command::Lidarr(LidarrCommand::ManualSearch(value))
}
}
pub(super) struct LidarrManualSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrManualSearchCommand>
for LidarrManualSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrManualSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrManualSearchCommand::Album {
artist_id,
album_id,
} => {
println!("Searching for album releases. This may take a minute...");
match self
.network
.handle_network_event(LidarrEvent::GetAlbumReleases(artist_id, album_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => {
let albums_vec: Vec<LidarrRelease> = releases_vec
.into_iter()
.filter(|release| !release.discography)
.collect();
serde_json::to_string_pretty(&albums_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
LidarrManualSearchCommand::Discography { artist_id } => {
println!("Searching for artist discography releases. This may take a minute...");
match self
.network
.handle_network_event(LidarrEvent::GetDiscographyReleases(artist_id).into())
.await
{
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => {
let discography_vec: Vec<LidarrRelease> = releases_vec
.into_iter()
.filter(|release| release.discography)
.collect();
serde_json::to_string_pretty(&discography_vec)?
}
Err(e) => return Err(e),
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
}
}
};
Ok(result)
}
}
@@ -1,207 +0,0 @@
#[cfg(test)]
mod tests {
use crate::cli::Command;
use crate::cli::lidarr::LidarrCommand;
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
use pretty_assertions::assert_eq;
#[test]
fn test_lidarr_manual_search_command_from() {
let command = LidarrManualSearchCommand::Discography { artist_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Lidarr(LidarrCommand::ManualSearch(command))
);
}
mod cli {
use crate::Cli;
use clap::CommandFactory;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_manual_album_search_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--album-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_album_search_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--artist-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_album_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"album",
"--artist-id",
"1",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_manual_discography_search_requires_artist_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "manual-search", "discography"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_discography_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"manual-search",
"discography",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use crate::app::App;
use crate::cli::CliCommandHandler;
use crate::cli::lidarr::manual_search_command_handler::{
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::Serdeable;
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 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();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetAlbumReleases(expected_artist_id, expected_album_id).into(),
))
.times(1)
.returning(|_| {
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 {
artist_id: 1,
album_id: 1,
};
let result = LidarrManualSearchCommandHandler::with(
&app_arc,
manual_album_search_command,
&mut mock_network,
)
.handle()
.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
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
))
.times(1)
.returning(|_| {
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 =
LidarrManualSearchCommand::Discography { artist_id: 1 };
let result = LidarrManualSearchCommandHandler::with(
&app_arc,
manual_discography_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&expected_releases).unwrap()
);
}
}
}
-296
View File
@@ -1,296 +0,0 @@
use std::sync::Arc;
use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
use anyhow::Result;
use clap::Subcommand;
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler};
use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
use list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
use serde_json::json;
use tokio::sync::Mutex;
use trigger_automatic_search_command_handler::{
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
};
use super::{CliCommandHandler, Command};
use crate::cli::lidarr::manual_search_command_handler::{
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
};
use crate::models::lidarr_models::LidarrTaskName;
use crate::models::servarr_models::ReleaseDownloadBody;
use crate::network::lidarr_network::LidarrEvent;
use crate::{app::App, network::NetworkTrait};
mod add_command_handler;
mod delete_command_handler;
mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod manual_search_command_handler;
mod refresh_command_handler;
mod trigger_automatic_search_command_handler;
#[cfg(test)]
#[path = "lidarr_command_tests.rs"]
mod lidarr_command_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrCommand {
#[command(
subcommand,
about = "Commands to add or create new resources within your Lidarr instance"
)]
Add(LidarrAddCommand),
#[command(
subcommand,
about = "Commands to delete resources from your Lidarr instance"
)]
Delete(LidarrDeleteCommand),
#[command(
subcommand,
about = "Commands to edit resources in your Lidarr instance"
)]
Edit(LidarrEditCommand),
#[command(
subcommand,
about = "Commands to fetch details of the resources in your Lidarr instance"
)]
Get(LidarrGetCommand),
#[command(
subcommand,
about = "Commands to list attributes from your Lidarr instance"
)]
List(LidarrListCommand),
#[command(
subcommand,
about = "Commands to refresh the data in your Lidarr instance"
)]
Refresh(LidarrRefreshCommand),
#[command(subcommand, about = "Commands to manually search for releases")]
ManualSearch(LidarrManualSearchCommand),
#[command(
subcommand,
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)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
},
#[command(about = "Mark the Lidarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
long,
help = "The Lidarr ID of the history item you wish to mark as 'failed'",
required = true
)]
history_item_id: i64,
},
#[command(about = "Search for a new artist to add to Lidarr")]
SearchNewArtist {
#[arg(
long,
help = "The name of the artist you want to search for",
required = true
)]
query: String,
},
#[command(about = "Start the specified Lidarr task")]
StartTask {
#[arg(
long,
help = "The name of the task to trigger",
value_enum,
required = true
)]
task_name: LidarrTaskName,
},
#[command(
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
)]
TestIndexer {
#[arg(long, help = "The ID of the indexer to test", required = true)]
indexer_id: i64,
},
#[command(about = "Test all Lidarr indexers")]
TestAllIndexers,
#[command(
about = "Toggle monitoring for the specified album corresponding to the given album ID"
)]
ToggleAlbumMonitoring {
#[arg(
long,
help = "The Lidarr ID of the album to toggle monitoring on",
required = true
)]
album_id: i64,
},
#[command(
about = "Toggle monitoring for the specified artist corresponding to the given artist ID"
)]
ToggleArtistMonitoring {
#[arg(
long,
help = "The Lidarr ID of the artist to toggle monitoring on",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrCommand> for Command {
fn from(lidarr_command: LidarrCommand) -> Command {
Command::Lidarr(lidarr_command)
}
}
pub(super) struct LidarrCliHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: LidarrCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrCliHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrCommand::Add(add_command) => {
LidarrAddCommandHandler::with(self.app, add_command, self.network)
.handle()
.await?
}
LidarrCommand::Delete(delete_command) => {
LidarrDeleteCommandHandler::with(self.app, delete_command, self.network)
.handle()
.await?
}
LidarrCommand::Edit(edit_command) => {
LidarrEditCommandHandler::with(self.app, edit_command, self.network)
.handle()
.await?
}
LidarrCommand::Get(get_command) => {
LidarrGetCommandHandler::with(self.app, get_command, self.network)
.handle()
.await?
}
LidarrCommand::List(list_command) => {
LidarrListCommandHandler::with(self.app, list_command, self.network)
.handle()
.await?
}
LidarrCommand::Refresh(refresh_command) => {
LidarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
.handle()
.await?
}
LidarrCommand::ManualSearch(manual_search_command) => {
LidarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network)
.handle()
.await?
}
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
LidarrTriggerAutomaticSearchCommandHandler::with(
self.app,
trigger_automatic_search_command,
self.network,
)
.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 = ReleaseDownloadBody { guid, indexer_id };
let resp = self
.network
.handle_network_event(LidarrEvent::DownloadRelease(params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
.handle_network_event(LidarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
serde_json::to_string_pretty(&json!({"message": "Lidarr history item marked as 'failed'"}))?
}
LidarrCommand::SearchNewArtist { query } => {
let resp = self
.network
.handle_network_event(LidarrEvent::SearchNewArtist(query).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::StartTask { task_name } => {
let resp = self
.network
.handle_network_event(LidarrEvent::StartTask(task_name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::TestIndexer { indexer_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::TestIndexer(indexer_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::TestAllIndexers => {
println!("Testing all Lidarr indexers. This may take a minute...");
let resp = self
.network
.handle_network_event(LidarrEvent::TestAllIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::ToggleAlbumMonitoring { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::ToggleAlbumMonitoring(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrCommand::ToggleArtistMonitoring { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::ToggleArtistMonitoring(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
-89
View File
@@ -1,89 +0,0 @@
use std::sync::Arc;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "refresh_command_handler_tests.rs"]
mod refresh_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrRefreshCommand {
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
AllArtists,
#[command(about = "Refresh artist data and scan disk for the artist with the given ID")]
Artist {
#[arg(
long,
help = "The ID of the artist to refresh information on and to scan the disk for",
required = true
)]
artist_id: i64,
},
#[command(about = "Refresh all downloads in Lidarr")]
Downloads,
}
impl From<LidarrRefreshCommand> for Command {
fn from(value: LidarrRefreshCommand) -> Self {
Command::Lidarr(LidarrCommand::Refresh(value))
}
}
pub(super) struct LidarrRefreshCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
for LidarrRefreshCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrRefreshCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> anyhow::Result<String> {
let result = match self.command {
LidarrRefreshCommand::AllArtists => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateAllArtists.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrRefreshCommand::Artist { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateAndScanArtist(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrRefreshCommand::Downloads => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,138 +0,0 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::Cli;
use crate::cli::{
Command,
lidarr::{LidarrCommand, refresh_command_handler::LidarrRefreshCommand},
};
use clap::CommandFactory;
#[test]
fn test_lidarr_refresh_command_from() {
let command = LidarrRefreshCommand::AllArtists;
let result = Command::from(command.clone());
assert_eq!(result, Command::Lidarr(LidarrCommand::Refresh(command)));
}
mod cli {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_refresh_commands_have_no_arg_requirements(
#[values("all-artists", "downloads")] subcommand: &str,
) {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", subcommand]);
assert_ok!(&result);
}
#[test]
fn test_refresh_artist_requires_artist_id() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "artist"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_refresh_artist_with_artist_id() {
let expected_args = LidarrRefreshCommand::Artist { artist_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"refresh",
"artist",
"--artist-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Refresh(refresh_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(refresh_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{app::App, cli::lidarr::refresh_command_handler::LidarrRefreshCommandHandler};
use crate::{
cli::{CliCommandHandler, lidarr::refresh_command_handler::LidarrRefreshCommand},
network::lidarr_network::LidarrEvent,
};
use crate::{
models::{Serdeable, lidarr_models::LidarrSerdeable},
network::{MockNetworkTrait, NetworkEvent},
};
#[rstest]
#[case(LidarrRefreshCommand::AllArtists, LidarrEvent::UpdateAllArtists)]
#[case(LidarrRefreshCommand::Downloads, LidarrEvent::UpdateDownloads)]
#[tokio::test]
async fn test_handle_refresh_command(
#[case] refresh_command: LidarrRefreshCommand,
#[case] expected_sonarr_event: LidarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_refresh_artist_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::UpdateAndScanArtist(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = LidarrRefreshCommand::Artist { artist_id: 1 };
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -1,88 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
use super::LidarrCommand;
#[cfg(test)]
#[path = "trigger_automatic_search_command_handler_tests.rs"]
mod trigger_automatic_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum LidarrTriggerAutomaticSearchCommand {
#[command(about = "Trigger an automatic search for the album with the specified ID")]
Album {
#[arg(
long,
help = "The Lidarr ID of the album you want to trigger an automatic search for",
required = true
)]
album_id: i64,
},
#[command(about = "Trigger an automatic search for the artist with the specified ID")]
Artist {
#[arg(
long,
help = "The ID of the artist you want to trigger an automatic search for",
required = true
)]
artist_id: i64,
},
}
impl From<LidarrTriggerAutomaticSearchCommand> for Command {
fn from(value: LidarrTriggerAutomaticSearchCommand) -> Self {
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(value))
}
}
pub(super) struct LidarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrTriggerAutomaticSearchCommand>
for LidarrTriggerAutomaticSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: LidarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
LidarrTriggerAutomaticSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
LidarrTriggerAutomaticSearchCommand::Album { album_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::TriggerAutomaticAlbumSearch(album_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrTriggerAutomaticSearchCommand::Artist { artist_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::TriggerAutomaticArtistSearch(artist_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,166 +0,0 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::Cli;
use crate::cli::{
Command,
lidarr::{
LidarrCommand, trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand,
},
};
use clap::CommandFactory;
#[test]
fn test_lidarr_trigger_automatic_search_command_from() {
let command = LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(command))
);
}
mod cli {
use super::*;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_trigger_automatic_album_search_requires_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"album",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_album_search_with_album_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"album",
"--album-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_trigger_automatic_artist_search_requires_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"artist",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_artist_search_with_artist_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"lidarr",
"trigger-automatic-search",
"artist",
"--artist-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::lidarr::trigger_automatic_search_command_handler::{
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
};
use crate::{app::App, cli::CliCommandHandler};
use crate::{
models::{Serdeable, lidarr_models::LidarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
};
#[tokio::test]
async fn test_handle_trigger_automatic_album_search_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticAlbumSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrTriggerAutomaticSearchCommand::Album { album_id: 1 };
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_trigger_automatic_artist_search_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
+1 -28
View File
@@ -1,20 +1,15 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use clap::{Subcommand, command};
use clap_complete::Shell;
use indoc::indoc;
use lidarr::{LidarrCliHandler, LidarrCommand};
use radarr::{RadarrCliHandler, RadarrCommand};
use readarr::{ReadarrCliHandler, ReadarrCommand};
use sonarr::{SonarrCliHandler, SonarrCommand};
use tokio::sync::Mutex;
use crate::{app::App, network::NetworkTrait};
pub mod lidarr;
pub mod radarr;
pub mod readarr;
pub mod sonarr;
#[cfg(test)]
@@ -29,12 +24,6 @@ pub enum Command {
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
Sonarr(SonarrCommand),
#[command(subcommand, about = "Commands for manging your Lidarr instance")]
Lidarr(LidarrCommand),
#[command(subcommand, about = "Commands for managing your Readarr instance")]
Readarr(ReadarrCommand),
#[command(
arg_required_else_help = true,
about = "Generate shell completions for the Managarr CLI"
@@ -49,12 +38,6 @@ pub enum Command {
#[arg(long, help = "Disable colored log output")]
no_color: bool,
},
#[command(about = indoc!{"
Print the full path to the configuration file in use.
This is the default configuration file location unless overridden with the '--config-file' flag
"})]
ConfigPath,
}
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
@@ -78,16 +61,6 @@ pub(crate) async fn handle_command(
.handle()
.await?
}
Command::Lidarr(lidarr_command) => {
LidarrCliHandler::with(app, lidarr_command, network)
.handle()
.await?
}
Command::Readarr(readarr_command) => {
ReadarrCliHandler::with(app, readarr_command, network)
.handle()
.await?
}
_ => String::new(),
};
+3 -3
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, Subcommand};
use clap::{ArgAction, Subcommand, arg, command};
use tokio::sync::Mutex;
use super::RadarrCommand;
@@ -122,12 +122,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
title: String::new(),
root_folder_path,
quality_profile_id,
minimum_availability,
minimum_availability: minimum_availability.to_string(),
monitored: !disable_monitoring,
tags,
tag_input_string: None,
add_options: AddMovieOptions {
monitor,
monitor: monitor.to_string(),
search_for_movie: !no_search_for_movie,
},
};
+2 -2
View File
@@ -384,12 +384,12 @@ mod tests {
title: String::new(),
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
minimum_availability: MinimumAvailability::Released,
minimum_availability: "released".to_owned(),
monitored: false,
tags: vec![1, 2],
tag_input_string: None,
add_options: AddMovieOptions {
monitor: MovieMonitor::MovieAndCollection,
monitor: "movieAndCollection".to_owned(),
search_for_movie: false,
},
};
+45 -45
View File
@@ -833,13 +833,13 @@ mod tests {
async fn test_handle_edit_all_indexer_settings_command() {
let expected_edit_all_indexer_settings = IndexerSettings {
allow_hardcoded_subs: true,
availability_delay: 19,
availability_delay: 1,
id: 1,
maximum_size: 18400,
minimum_age: 27,
maximum_size: 1,
minimum_age: 1,
prefer_indexer_flags: true,
retention: 44,
rss_sync_interval: 60,
retention: 1,
rss_sync_interval: 1,
whitelisted_hardcoded_subs: "test".into(),
};
let mut mock_network = MockNetworkTrait::new();
@@ -853,13 +853,13 @@ mod tests {
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
IndexerSettings {
allow_hardcoded_subs: false,
availability_delay: 31,
id: 8,
maximum_size: 22750,
minimum_age: 52,
availability_delay: 2,
id: 1,
maximum_size: 2,
minimum_age: 2,
prefer_indexer_flags: false,
retention: 66,
rss_sync_interval: 90,
retention: 2,
rss_sync_interval: 2,
whitelisted_hardcoded_subs: "testing".into(),
},
)))
@@ -879,13 +879,13 @@ mod tests {
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs: true,
disable_allow_hardcoded_subs: false,
availability_delay: Some(19),
maximum_size: Some(18400),
minimum_age: Some(27),
availability_delay: Some(1),
maximum_size: Some(1),
minimum_age: Some(1),
prefer_indexer_flags: true,
disable_prefer_indexer_flags: false,
retention: Some(44),
rss_sync_interval: Some(60),
retention: Some(1),
rss_sync_interval: Some(1),
whitelisted_subtitle_tags: Some("test".to_owned()),
};
@@ -904,13 +904,13 @@ mod tests {
async fn test_handle_edit_all_indexer_settings_command_disable_flags_function_properly() {
let expected_edit_all_indexer_settings = IndexerSettings {
allow_hardcoded_subs: false,
availability_delay: 19,
availability_delay: 1,
id: 1,
maximum_size: 18400,
minimum_age: 27,
maximum_size: 1,
minimum_age: 1,
prefer_indexer_flags: false,
retention: 44,
rss_sync_interval: 60,
retention: 1,
rss_sync_interval: 1,
whitelisted_hardcoded_subs: "test".into(),
};
let mut mock_network = MockNetworkTrait::new();
@@ -924,13 +924,13 @@ mod tests {
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
IndexerSettings {
allow_hardcoded_subs: true,
availability_delay: 31,
id: 8,
maximum_size: 22750,
minimum_age: 52,
availability_delay: 2,
id: 1,
maximum_size: 2,
minimum_age: 2,
prefer_indexer_flags: true,
retention: 66,
rss_sync_interval: 90,
retention: 2,
rss_sync_interval: 2,
whitelisted_hardcoded_subs: "testing".into(),
},
)))
@@ -950,13 +950,13 @@ mod tests {
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs: false,
disable_allow_hardcoded_subs: true,
availability_delay: Some(19),
maximum_size: Some(18400),
minimum_age: Some(27),
availability_delay: Some(1),
maximum_size: Some(1),
minimum_age: Some(1),
prefer_indexer_flags: false,
disable_prefer_indexer_flags: true,
retention: Some(44),
rss_sync_interval: Some(60),
retention: Some(1),
rss_sync_interval: Some(1),
whitelisted_subtitle_tags: Some("test".to_owned()),
};
@@ -976,13 +976,13 @@ mod tests {
{
let expected_edit_all_indexer_settings = IndexerSettings {
allow_hardcoded_subs: true,
availability_delay: 31,
availability_delay: 2,
id: 1,
maximum_size: 22750,
minimum_age: 52,
prefer_indexer_flags: false,
retention: 66,
rss_sync_interval: 90,
maximum_size: 2,
minimum_age: 2,
prefer_indexer_flags: true,
retention: 2,
rss_sync_interval: 2,
whitelisted_hardcoded_subs: "testing".into(),
};
let mut mock_network = MockNetworkTrait::new();
@@ -996,13 +996,13 @@ mod tests {
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
IndexerSettings {
allow_hardcoded_subs: true,
availability_delay: 31,
id: 8,
maximum_size: 22750,
minimum_age: 52,
prefer_indexer_flags: false,
retention: 66,
rss_sync_interval: 90,
availability_delay: 2,
id: 1,
maximum_size: 2,
minimum_age: 2,
prefer_indexer_flags: true,
retention: 2,
rss_sync_interval: 2,
whitelisted_hardcoded_subs: "testing".into(),
},
)))
+1 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use clap::{Subcommand, command};
use tokio::sync::Mutex;
use crate::{
+1 -13
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use clap::{Subcommand, command};
use tokio::sync::Mutex;
use crate::{
@@ -29,11 +29,6 @@ pub enum RadarrListCommand {
},
#[command(about = "List disk space details for all provisioned root folders in Radarr")]
DiskSpace,
#[command(about = "Fetch all Radarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
events: u64,
},
#[command(about = "List all Radarr indexers")]
Indexers,
#[command(about = "Fetch Radarr logs")]
@@ -126,13 +121,6 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::History { events: items } => {
let resp = self
.network
.handle_network_event(RadarrEvent::GetHistory(items).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Indexers => {
let resp = self
.network
@@ -111,29 +111,6 @@ mod tests {
assert_eq!(refresh_command, expected_args);
}
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "history", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_history_default_values() {
let expected_args = RadarrListCommand::History { events: 500 };
let result = Cli::try_parse_from(["managarr", "radarr", "list", "history"]);
assert_ok!(&result);
let Some(Command::Radarr(RadarrCommand::List(history_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(history_command, expected_args);
}
#[test]
fn test_list_logs_default_values() {
let expected_args = RadarrListCommand::Logs {
@@ -256,32 +233,6 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_history_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetHistory(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_history_command = RadarrListCommand::History { events: 1000 };
let result =
RadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_logs_command() {
let expected_events = 1000;
-18
View File
@@ -64,15 +64,6 @@ pub enum RadarrCommand {
Refresh(RadarrRefreshCommand),
#[command(about = "Clear the blocklist")]
ClearBlocklist,
#[command(about = "Mark the Radarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
long,
help = "The Radarr ID of the history item you wish to mark as 'failed'",
required = true
)]
history_item_id: i64,
},
#[command(about = "Manually download the given release for the specified movie ID")]
DownloadRelease {
#[arg(long, help = "The GUID of the release to download", required = true)]
@@ -217,15 +208,6 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
.handle_network_event(RadarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
serde_json::to_string_pretty(
&serde_json::json!({"message": "Radarr history item marked as 'failed'"}),
)?
}
RadarrCommand::DownloadRelease {
guid,
indexer_id,
-55
View File
@@ -31,31 +31,6 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_mark_history_item_as_failed_requires_history_item_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "mark-history-item-as-failed"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_mark_history_item_as_failed_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"mark-history-item-as-failed",
"--history-item-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_download_release_requires_movie_id() {
let result = Cli::command().try_get_matches_from([
@@ -352,36 +327,6 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_mark_history_item_as_failed_command() {
let expected_history_item_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let mark_history_item_as_failed_command =
RadarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 };
let result = RadarrCliHandler::with(
&app_arc,
mark_history_item_as_failed_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_download_release_command() {
let expected_release_download_body = RadarrReleaseDownloadBody {
-224
View File
@@ -1,224 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, Subcommand};
use tokio::sync::Mutex;
use super::ReadarrCommand;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::readarr_models::{
AddAuthorBody, AddAuthorOptions, AddReadarrRootFolderBody, MonitorType, NewItemMonitorType,
},
network::{NetworkTrait, readarr_network::ReadarrEvent},
};
#[cfg(test)]
#[path = "add_command_handler_tests.rs"]
mod add_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrAddCommand {
#[command(about = "Add a new author to your Readarr library")]
Author {
#[arg(
long,
help = "The Goodreads foreign author ID of the author you wish to add to your library",
required = true
)]
foreign_author_id: String,
#[arg(long, help = "The name of the author", required = true)]
author_name: String,
#[arg(
long,
help = "The root folder path where all author data and metadata should live",
required = true
)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the quality profile to use for this author",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the metadata profile to use for this author",
required = true
)]
metadata_profile_id: i64,
#[arg(long, help = "Disable monitoring for this author")]
disable_monitoring: bool,
#[arg(
long,
help = "Tag IDs to tag the author with",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
#[arg(
long,
help = "What Readarr should monitor for this author",
value_enum,
default_value_t = MonitorType::default()
)]
monitor: MonitorType,
#[arg(
long,
help = "How Readarr should monitor new items for this author",
value_enum,
default_value_t = NewItemMonitorType::default()
)]
monitor_new_items: NewItemMonitorType,
#[arg(
long,
help = "Tell Readarr to not start a search for missing books once the author is added to your library"
)]
no_search_for_missing_books: bool,
},
#[command(about = "Add a new root folder")]
RootFolder {
#[arg(long, help = "The name of the root folder", required = true)]
name: String,
#[arg(long, help = "The path of the new root folder", required = true)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the default quality profile for authors in this root folder",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the default metadata profile for authors in this root folder",
required = true
)]
metadata_profile_id: i64,
#[arg(
long,
help = "The default monitor option for authors in this root folder",
value_enum,
default_value_t = MonitorType::default()
)]
monitor: MonitorType,
#[arg(
long,
help = "The default monitor new items option for authors in this root folder",
value_enum,
default_value_t = NewItemMonitorType::default()
)]
monitor_new_items: NewItemMonitorType,
#[arg(
long,
help = "Default tag IDs for authors in this root folder",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
},
#[command(about = "Add new tag")]
Tag {
#[arg(long, help = "The name of the tag to be added", required = true)]
name: String,
},
}
impl From<ReadarrAddCommand> for Command {
fn from(value: ReadarrAddCommand) -> Self {
Command::Readarr(ReadarrCommand::Add(value))
}
}
pub(super) struct ReadarrAddCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrAddCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrAddCommand> for ReadarrAddCommandHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrAddCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrAddCommandHandler {
_app: app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrAddCommand::Author {
foreign_author_id,
author_name,
root_folder_path,
quality_profile_id,
metadata_profile_id,
disable_monitoring,
tag: tags,
monitor,
monitor_new_items,
no_search_for_missing_books,
} => {
let body = AddAuthorBody {
foreign_author_id,
author_name,
monitored: !disable_monitoring,
root_folder_path,
quality_profile_id,
metadata_profile_id,
tags,
images: Vec::new(),
tag_input_string: None,
add_options: AddAuthorOptions {
monitor,
monitor_new_items,
search_for_missing_books: !no_search_for_missing_books,
},
};
let resp = self
.network
.handle_network_event(ReadarrEvent::AddAuthor(body).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrAddCommand::RootFolder {
name,
root_folder_path,
quality_profile_id,
metadata_profile_id,
monitor,
monitor_new_items,
tag: tags,
} => {
let add_root_folder_body = AddReadarrRootFolderBody {
name,
path: root_folder_path,
default_quality_profile_id: quality_profile_id,
default_metadata_profile_id: metadata_profile_id,
default_monitor_option: monitor,
default_new_item_monitor_option: monitor_new_items,
default_tags: tags,
tag_input_string: None,
};
let resp = self
.network
.handle_network_event(ReadarrEvent::AddRootFolder(add_root_folder_body).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrAddCommand::Tag { name } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::AddTag(name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,562 +0,0 @@
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser, error::ErrorKind};
use crate::{
Cli,
cli::{
Command,
readarr::{ReadarrCommand, add_command_handler::ReadarrAddCommand},
},
models::readarr_models::{MonitorType, NewItemMonitorType},
};
use pretty_assertions::assert_eq;
#[test]
fn test_readarr_add_command_from() {
let command = ReadarrAddCommand::Tag {
name: String::new(),
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Readarr(ReadarrCommand::Add(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_add_author_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "add", "author"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_author_requires_foreign_author_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"add",
"author",
"--author-name",
"Test",
"--root-folder-path",
"/nfs/books",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_author_requires_author_name() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--root-folder-path",
"/nfs/books",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_author_requires_root_folder_path() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--author-name",
"Test",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_author_requires_quality_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--author-name",
"Test",
"--root-folder-path",
"/nfs/books",
"--metadata-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_author_requires_metadata_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--author-name",
"Test",
"--root-folder-path",
"/nfs/books",
"--quality-profile-id",
"1",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_author_success_with_required_args_only() {
let expected_args = ReadarrAddCommand::Author {
foreign_author_id: "test-id".to_owned(),
author_name: "Test Author".to_owned(),
root_folder_path: "/nfs/books".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
disable_monitoring: false,
tag: vec![],
monitor: MonitorType::default(),
monitor_new_items: NewItemMonitorType::default(),
no_search_for_missing_books: false,
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--author-name",
"Test Author",
"--root-folder-path",
"/nfs/books",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_author_success_with_all_args() {
let expected_args = ReadarrAddCommand::Author {
foreign_author_id: "test-id".to_owned(),
author_name: "Test Author".to_owned(),
root_folder_path: "/nfs/books".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 2,
disable_monitoring: true,
tag: vec![1, 2],
monitor: MonitorType::Future,
monitor_new_items: NewItemMonitorType::New,
no_search_for_missing_books: true,
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--author-name",
"Test Author",
"--root-folder-path",
"/nfs/books",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--disable-monitoring",
"--tag",
"1",
"--tag",
"2",
"--monitor",
"future",
"--monitor-new-items",
"new",
"--no-search-for-missing-books",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_author_monitor_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--author-name",
"Test Author",
"--root-folder-path",
"/nfs/books",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--monitor",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_author_new_item_monitor_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--author-name",
"Test Author",
"--root-folder-path",
"/nfs/books",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--monitor-new-items",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_author_tags_is_repeatable() {
let expected_args = ReadarrAddCommand::Author {
foreign_author_id: "test-id".to_owned(),
author_name: "Test Author".to_owned(),
root_folder_path: "/nfs/books".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 2,
disable_monitoring: false,
tag: vec![1, 2],
monitor: MonitorType::default(),
monitor_new_items: NewItemMonitorType::default(),
no_search_for_missing_books: false,
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"add",
"author",
"--foreign-author-id",
"test-id",
"--author-name",
"Test Author",
"--root-folder-path",
"/nfs/books",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"2",
"--tag",
"1",
"--tag",
"2",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "add", "root-folder"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_root_folder_success() {
let expected_args = ReadarrAddCommand::RootFolder {
name: "Books".to_owned(),
root_folder_path: "/nfs/test".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
tag: vec![],
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"add",
"root-folder",
"--name",
"Books",
"--root-folder-path",
"/nfs/test",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "add", "tag"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_tag_success() {
let expected_args = ReadarrAddCommand::Tag {
name: "test".to_owned(),
};
let result = Cli::try_parse_from(["managarr", "readarr", "add", "tag", "--name", "test"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(add_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::CliCommandHandler;
use crate::cli::readarr::add_command_handler::{ReadarrAddCommand, ReadarrAddCommandHandler};
use crate::models::Serdeable;
use crate::models::readarr_models::{
AddAuthorBody, AddAuthorOptions, AddReadarrRootFolderBody, MonitorType, NewItemMonitorType,
ReadarrSerdeable,
};
use crate::network::readarr_network::ReadarrEvent;
use crate::{
app::App,
network::{MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_add_author_command() {
let expected_body = AddAuthorBody {
foreign_author_id: "test-id".to_owned(),
author_name: "Test Author".to_owned(),
monitored: false,
root_folder_path: "/nfs/books".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
tags: vec![1, 2],
images: Vec::new(),
tag_input_string: None,
add_options: AddAuthorOptions {
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
search_for_missing_books: false,
},
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::AddAuthor(expected_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_author_command = ReadarrAddCommand::Author {
foreign_author_id: "test-id".to_owned(),
author_name: "Test Author".to_owned(),
root_folder_path: "/nfs/books".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
disable_monitoring: true,
tag: vec![1, 2],
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
no_search_for_missing_books: true,
};
let result = ReadarrAddCommandHandler::with(&app_arc, add_author_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_add_root_folder_command() {
let expected_root_folder_path = "/nfs/test".to_owned();
let expected_add_root_folder_body = AddReadarrRootFolderBody {
name: "Books".to_owned(),
path: expected_root_folder_path.clone(),
default_quality_profile_id: 1,
default_metadata_profile_id: 1,
default_monitor_option: MonitorType::All,
default_new_item_monitor_option: NewItemMonitorType::All,
default_tags: vec![1, 2],
tag_input_string: None,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_root_folder_command = ReadarrAddCommand::RootFolder {
name: "Books".to_owned(),
root_folder_path: expected_root_folder_path,
quality_profile_id: 1,
metadata_profile_id: 1,
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
tag: vec![1, 2],
};
let result =
ReadarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_add_tag_command() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = ReadarrAddCommand::Tag {
name: expected_tag_name,
};
let result = ReadarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
-182
View File
@@ -1,182 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use super::ReadarrCommand;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::readarr_models::DeleteParams,
network::{NetworkTrait, readarr_network::ReadarrEvent},
};
#[cfg(test)]
#[path = "delete_command_handler_tests.rs"]
mod delete_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrDeleteCommand {
#[command(about = "Delete an author from your Readarr library")]
Author {
#[arg(long, help = "The ID of the author to delete", required = true)]
author_id: i64,
#[arg(long, help = "Delete the author files from disk as well")]
delete_files_from_disk: bool,
#[arg(long, help = "Add a list exclusion for this author")]
add_list_exclusion: bool,
},
#[command(about = "Delete the specified item from the Readarr 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 a book from your Readarr library")]
Book {
#[arg(long, help = "The ID of the book to delete", required = true)]
book_id: i64,
#[arg(long, help = "Delete the book files from disk as well")]
delete_files_from_disk: bool,
#[arg(long, help = "Add a list exclusion for this book")]
add_list_exclusion: bool,
},
#[command(about = "Delete the specified book file from disk")]
BookFile {
#[arg(long, help = "The ID of the book file to delete", required = true)]
book_file_id: i64,
},
#[command(about = "Delete the specified download")]
Download {
#[arg(long, help = "The ID of the download to delete", required = true)]
download_id: i64,
},
#[command(about = "Delete the indexer with the given ID")]
Indexer {
#[arg(long, help = "The ID of the indexer to delete", required = true)]
indexer_id: i64,
},
#[command(about = "Delete the root folder with the given ID")]
RootFolder {
#[arg(long, help = "The ID of the root folder to delete", required = true)]
root_folder_id: i64,
},
#[command(about = "Delete the tag with the specified ID")]
Tag {
#[arg(long, help = "The ID of the tag to delete", required = true)]
tag_id: i64,
},
}
impl From<ReadarrDeleteCommand> for Command {
fn from(value: ReadarrDeleteCommand) -> Self {
Command::Readarr(ReadarrCommand::Delete(value))
}
}
pub(super) struct ReadarrDeleteCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrDeleteCommand>
for ReadarrDeleteCommandHandler<'a, 'b>
{
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrDeleteCommandHandler {
_app: app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrDeleteCommand::Author {
author_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_author_params = DeleteParams {
id: author_id,
delete_files: delete_files_from_disk,
add_import_list_exclusion: add_list_exclusion,
};
let resp = self
.network
.handle_network_event(ReadarrEvent::DeleteAuthor(delete_author_params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::DeleteBlocklistItem(blocklist_item_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrDeleteCommand::Book {
book_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_book_params = DeleteParams {
id: book_id,
delete_files: delete_files_from_disk,
add_import_list_exclusion: add_list_exclusion,
};
let resp = self
.network
.handle_network_event(ReadarrEvent::DeleteBook(delete_book_params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrDeleteCommand::BookFile { book_file_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::DeleteBookFile(book_file_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrDeleteCommand::Download { download_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::DeleteDownload(download_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrDeleteCommand::Indexer { indexer_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::DeleteIndexer(indexer_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrDeleteCommand::RootFolder { root_folder_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::DeleteRootFolder(root_folder_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrDeleteCommand::Tag { tag_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::DeleteTag(tag_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,701 +0,0 @@
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser, error::ErrorKind};
use crate::{
Cli,
cli::{
Command,
readarr::{ReadarrCommand, delete_command_handler::ReadarrDeleteCommand},
},
};
use pretty_assertions::assert_eq;
#[test]
fn test_readarr_delete_command_from() {
let command = ReadarrDeleteCommand::Tag { tag_id: 1 };
let result = Command::from(command.clone());
assert_eq!(result, Command::Readarr(ReadarrCommand::Delete(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_delete_author_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "author"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_author_author_id_requires_a_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"delete",
"author",
"--author-id",
"not_a_number",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
}
#[test]
fn test_delete_author_defaults() {
let expected_args = ReadarrDeleteCommand::Author {
author_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"delete",
"author",
"--author-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_author_all_args_defined() {
let expected_args = ReadarrDeleteCommand::Author {
author_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"delete",
"author",
"--author-id",
"1",
"--delete-files-from-disk",
"--add-list-exclusion",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_blocklist_item_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "blocklist-item"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_blocklist_item_blocklist_item_id_requires_a_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"delete",
"blocklist-item",
"--blocklist-item-id",
"not_a_number",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
}
#[test]
fn test_delete_blocklist_item_success() {
let expected_args = ReadarrDeleteCommand::BlocklistItem {
blocklist_item_id: 7,
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"delete",
"blocklist-item",
"--blocklist-item-id",
"7",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_book_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "book"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_book_book_id_requires_a_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"delete",
"book",
"--book-id",
"not_a_number",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
}
#[test]
fn test_delete_book_defaults() {
let expected_args = ReadarrDeleteCommand::Book {
book_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result = Cli::try_parse_from(["managarr", "readarr", "delete", "book", "--book-id", "1"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_book_all_args_defined() {
let expected_args = ReadarrDeleteCommand::Book {
book_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"delete",
"book",
"--book-id",
"1",
"--delete-files-from-disk",
"--add-list-exclusion",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_book_file_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "book-file"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_book_file_book_file_id_requires_a_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"delete",
"book-file",
"--book-file-id",
"not_a_number",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
}
#[test]
fn test_delete_book_file_success() {
let expected_args = ReadarrDeleteCommand::BookFile { book_file_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"delete",
"book-file",
"--book-file-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_download_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "download"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_download_download_id_requires_a_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"delete",
"download",
"--download-id",
"not_a_number",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
}
#[test]
fn test_delete_download_success() {
let expected_args = ReadarrDeleteCommand::Download { download_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"delete",
"download",
"--download-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_indexer_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "indexer"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_indexer_indexer_id_requires_a_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"delete",
"indexer",
"--indexer-id",
"not_a_number",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
}
#[test]
fn test_delete_indexer_success() {
let expected_args = ReadarrDeleteCommand::Indexer { indexer_id: 8 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"delete",
"indexer",
"--indexer-id",
"8",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "root-folder"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_root_folder_root_folder_id_requires_a_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"delete",
"root-folder",
"--root-folder-id",
"not_a_number",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
}
#[test]
fn test_delete_root_folder_success() {
let expected_args = ReadarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"delete",
"root-folder",
"--root-folder-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "tag"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_tag_tag_id_requires_a_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"delete",
"tag",
"--tag-id",
"not_a_number",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
}
#[test]
fn test_delete_tag_success() {
let expected_args = ReadarrDeleteCommand::Tag { tag_id: 1 };
let result = Cli::try_parse_from(["managarr", "readarr", "delete", "tag", "--tag-id", "1"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::CliCommandHandler;
use crate::cli::readarr::delete_command_handler::{
ReadarrDeleteCommand, ReadarrDeleteCommandHandler,
};
use crate::models::Serdeable;
use crate::models::readarr_models::{DeleteParams, ReadarrSerdeable};
use crate::network::readarr_network::ReadarrEvent;
use crate::{
app::App,
network::{MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_delete_author_command() {
let expected_delete_author_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::DeleteAuthor(expected_delete_author_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_author_command = ReadarrDeleteCommand::Author {
author_id: 1,
delete_files_from_disk: true,
add_list_exclusion: false,
};
let result =
ReadarrDeleteCommandHandler::with(&app_arc, delete_author_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_book_command() {
let expected_delete_book_params = DeleteParams {
id: 1,
delete_files: true,
add_import_list_exclusion: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::DeleteBook(expected_delete_book_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_book_command = ReadarrDeleteCommand::Book {
book_id: 1,
delete_files_from_disk: true,
add_list_exclusion: false,
};
let result =
ReadarrDeleteCommandHandler::with(&app_arc, delete_book_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_blocklist_item_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::DeleteBlocklistItem(7).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_blocklist_item_command = ReadarrDeleteCommand::BlocklistItem {
blocklist_item_id: 7,
};
let result = ReadarrDeleteCommandHandler::with(
&app_arc,
delete_blocklist_item_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_book_file_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteBookFile(1).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_book_file_command = ReadarrDeleteCommand::BookFile { book_file_id: 1 };
let result =
ReadarrDeleteCommandHandler::with(&app_arc, delete_book_file_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_download_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteDownload(1).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_download_command = ReadarrDeleteCommand::Download { download_id: 1 };
let result =
ReadarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_indexer_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteIndexer(8).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_indexer_command = ReadarrDeleteCommand::Indexer { indexer_id: 8 };
let result =
ReadarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_root_folder_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteRootFolder(1).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_root_folder_command = ReadarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result =
ReadarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_tag_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteTag(1).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_tag_command = ReadarrDeleteCommand::Tag { tag_id: 1 };
let result =
ReadarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
-344
View File
@@ -1,344 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, ArgGroup, Subcommand};
use tokio::sync::Mutex;
use super::ReadarrCommand;
use crate::models::Serdeable;
use crate::models::readarr_models::ReadarrSerdeable;
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
use crate::{
app::App,
cli::{CliCommandHandler, Command, mutex_flags_or_option},
models::readarr_models::{EditAuthorParams, NewItemMonitorType},
network::{NetworkTrait, readarr_network::ReadarrEvent},
};
#[cfg(test)]
#[path = "edit_command_handler_tests.rs"]
mod edit_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrEditCommand {
#[command(
about = "Edit the indexer settings that apply to all indexers",
group(
ArgGroup::new("edit_settings")
.args([
"maximum_size",
"minimum_age",
"retention",
"rss_sync_interval",
]).required(true)
.multiple(true))
)]
AllIndexerSettings {
#[arg(
long,
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
)]
maximum_size: Option<i64>,
#[arg(
long,
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
)]
minimum_age: Option<i64>,
#[arg(
long,
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
)]
retention: Option<i64>,
#[arg(
long,
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
)]
rss_sync_interval: Option<i64>,
},
#[command(
about = "Edit preferences for the specified author",
group(
ArgGroup::new("edit_author")
.args([
"enable_monitoring",
"disable_monitoring",
"monitor_new_items",
"quality_profile_id",
"metadata_profile_id",
"root_folder_path",
"tag",
"clear_tags"
]).required(true)
.multiple(true))
)]
Author {
#[arg(
long,
help = "The ID of the author whose settings you want to edit",
required = true
)]
author_id: i64,
#[arg(
long,
help = "Enable monitoring of this author in Readarr so Readarr will automatically download books from this author if they are available",
conflicts_with = "disable_monitoring"
)]
enable_monitoring: bool,
#[arg(
long,
help = "Disable monitoring of this author so Readarr does not automatically download books from this author if they are available",
conflicts_with = "enable_monitoring"
)]
disable_monitoring: bool,
#[arg(
long,
help = "How Readarr should monitor new books from this author",
value_enum
)]
monitor_new_items: Option<NewItemMonitorType>,
#[arg(long, help = "The ID of the quality profile to use for this author")]
quality_profile_id: Option<i64>,
#[arg(long, help = "The ID of the metadata profile to use for this author")]
metadata_profile_id: Option<i64>,
#[arg(
long,
help = "The root folder path where all author data and metadata should live"
)]
root_folder_path: Option<String>,
#[arg(
long,
help = "Tag IDs to tag this author with",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(long, help = "Clear all tags on this author", conflicts_with = "tag")]
clear_tags: bool,
},
#[command(
about = "Edit preferences for the specified indexer",
group(
ArgGroup::new("edit_indexer")
.args([
"name",
"enable_rss",
"disable_rss",
"enable_automatic_search",
"disable_automatic_search",
"enable_interactive_search",
"disable_interactive_search",
"url",
"api_key",
"seed_ratio",
"tag",
"priority",
"clear_tags"
]).required(true)
.multiple(true))
)]
Indexer {
#[arg(
long,
help = "The ID of the indexer whose settings you wish to edit",
required = true
)]
indexer_id: i64,
#[arg(long, help = "The name of the indexer")]
name: Option<String>,
#[arg(
long,
help = "Indicate to Readarr that this indexer should be used when Readarr periodically looks for releases via RSS Sync",
conflicts_with = "disable_rss"
)]
enable_rss: bool,
#[arg(
long,
help = "Disable using this indexer when Readarr periodically looks for releases via RSS Sync",
conflicts_with = "enable_rss"
)]
disable_rss: bool,
#[arg(
long,
help = "Indicate to Readarr that this indexer should be used when automatic searches are performed via the UI or by Readarr",
conflicts_with = "disable_automatic_search"
)]
enable_automatic_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Readarr",
conflicts_with = "enable_automatic_search"
)]
disable_automatic_search: bool,
#[arg(
long,
help = "Indicate to Readarr that this indexer should be used when an interactive search is used",
conflicts_with = "disable_interactive_search"
)]
enable_interactive_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever an interactive search is performed",
conflicts_with = "enable_interactive_search"
)]
disable_interactive_search: bool,
#[arg(long, help = "The URL of the indexer")]
url: Option<String>,
#[arg(long, help = "The API key used to access the indexer's API")]
api_key: Option<String>,
#[arg(
long,
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
)]
seed_ratio: Option<String>,
#[arg(
long,
help = "Only use this indexer for authors with at least one matching tag ID. Leave blank to use with all authors.",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(
long,
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Readarr will still use all enabled indexers for RSS Sync and Searching"
)]
priority: Option<i64>,
#[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")]
clear_tags: bool,
},
}
impl From<ReadarrEditCommand> for Command {
fn from(value: ReadarrEditCommand) -> Self {
Command::Readarr(ReadarrCommand::Edit(value))
}
}
pub(super) struct ReadarrEditCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrEditCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrEditCommand> for ReadarrEditCommandHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrEditCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrEditCommandHandler {
_app: app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrEditCommand::AllIndexerSettings {
maximum_size,
minimum_age,
retention,
rss_sync_interval,
} => {
if let Serdeable::Readarr(ReadarrSerdeable::IndexerSettings(previous_indexer_settings)) =
self
.network
.handle_network_event(ReadarrEvent::GetAllIndexerSettings.into())
.await?
{
let params = IndexerSettings {
id: 1,
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
retention: retention.unwrap_or(previous_indexer_settings.retention),
rss_sync_interval: rss_sync_interval
.unwrap_or(previous_indexer_settings.rss_sync_interval),
};
self
.network
.handle_network_event(ReadarrEvent::EditAllIndexerSettings(params).into())
.await?;
"All indexer settings updated".to_owned()
} else {
String::new()
}
}
ReadarrEditCommand::Author {
author_id,
enable_monitoring,
disable_monitoring,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tag,
clear_tags,
} => {
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
let edit_author_params = EditAuthorParams {
author_id,
monitored: monitored_value,
monitor_new_items,
quality_profile_id,
metadata_profile_id,
root_folder_path,
tags: tag,
tag_input_string: None,
clear_tags,
};
self
.network
.handle_network_event(ReadarrEvent::EditAuthor(edit_author_params).into())
.await?;
"Author updated".to_owned()
}
ReadarrEditCommand::Indexer {
indexer_id,
name,
enable_rss,
disable_rss,
enable_automatic_search,
disable_automatic_search,
enable_interactive_search,
disable_interactive_search,
url,
api_key,
seed_ratio,
tag,
priority,
clear_tags,
} => {
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
let automatic_search_value =
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
let interactive_search_value =
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
let edit_indexer_params = EditIndexerParams {
indexer_id,
name,
enable_rss: rss_value,
enable_automatic_search: automatic_search_value,
enable_interactive_search: interactive_search_value,
url,
api_key,
seed_ratio,
tags: tag,
tag_input_string: None,
priority,
clear_tags,
};
self
.network
.handle_network_event(ReadarrEvent::EditIndexer(edit_indexer_params).into())
.await?;
"Indexer updated".to_owned()
}
};
Ok(result)
}
}
File diff suppressed because it is too large. Load diff
-122
View File
@@ -1,122 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, readarr_network::ReadarrEvent},
};
use super::ReadarrCommand;
#[cfg(test)]
#[path = "get_command_handler_tests.rs"]
mod get_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrGetCommand {
#[command(about = "Get the shared settings for all indexers")]
AllIndexerSettings,
#[command(about = "Get detailed information for the author with the given ID")]
AuthorDetails {
#[arg(
long,
help = "The Readarr ID of the author whose details you wish to fetch",
required = true
)]
author_id: i64,
},
#[command(about = "Get detailed information for the book with the given ID")]
BookDetails {
#[arg(
long,
help = "The Readarr ID of the book whose details you wish to fetch",
required = true
)]
book_id: i64,
},
#[command(about = "Fetch the host config for your Readarr instance")]
HostConfig,
#[command(about = "Fetch the security config for your Readarr instance")]
SecurityConfig,
#[command(about = "Get the system status")]
SystemStatus,
}
impl From<ReadarrGetCommand> for Command {
fn from(value: ReadarrGetCommand) -> Self {
Command::Readarr(ReadarrCommand::Get(value))
}
}
pub(super) struct ReadarrGetCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrGetCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrGetCommand> for ReadarrGetCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrGetCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrGetCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrGetCommand::AllIndexerSettings => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetAllIndexerSettings.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrGetCommand::AuthorDetails { author_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetAuthorDetails(author_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrGetCommand::BookDetails { book_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetBookDetails(book_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrGetCommand::HostConfig => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetHostConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrGetCommand::SecurityConfig => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetSecurityConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrGetCommand::SystemStatus => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetStatus.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,263 +0,0 @@
#[cfg(test)]
mod tests {
use crate::Cli;
use crate::cli::{
Command,
readarr::{ReadarrCommand, get_command_handler::ReadarrGetCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_readarr_get_command_from() {
let command = ReadarrGetCommand::SystemStatus;
let result = Command::from(command.clone());
assert_eq!(result, Command::Readarr(ReadarrCommand::Get(command)));
}
mod cli {
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
#[test]
fn test_all_indexer_settings_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "all-indexer-settings"]);
assert_ok!(&result);
}
#[test]
fn test_author_details_requires_author_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "author-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_author_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"get",
"author-details",
"--author-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_book_details_requires_book_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "book-details"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_book_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"get",
"book-details",
"--book-id",
"1",
]);
assert_ok!(&result);
}
#[rstest]
fn test_get_commands_have_no_arg_requirements(
#[values("host-config", "security-config", "system-status")] subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "get", subcommand]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
CliCommandHandler,
readarr::get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler},
},
models::{Serdeable, readarr_models::ReadarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
};
#[tokio::test]
async fn test_handle_get_all_indexer_settings_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_all_indexer_settings_command = ReadarrGetCommand::AllIndexerSettings;
let result = ReadarrGetCommandHandler::with(
&app_arc,
get_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_author_details_command() {
let expected_author_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetAuthorDetails(expected_author_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_author_details_command = ReadarrGetCommand::AuthorDetails { author_id: 1 };
let result =
ReadarrGetCommandHandler::with(&app_arc, get_author_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_book_details_command() {
let expected_book_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetBookDetails(expected_book_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_book_details_command = ReadarrGetCommand::BookDetails { book_id: 1 };
let result =
ReadarrGetCommandHandler::with(&app_arc, get_book_details_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_host_config_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetHostConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_host_config_command = ReadarrGetCommand::HostConfig;
let result =
ReadarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_security_config_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetSecurityConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_security_config_command = ReadarrGetCommand::SecurityConfig;
let result =
ReadarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_get_system_status_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = ReadarrGetCommand::SystemStatus;
let result =
ReadarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
-323
View File
@@ -1,323 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use super::ReadarrCommand;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, readarr_network::ReadarrEvent},
};
#[cfg(test)]
#[path = "list_command_handler_tests.rs"]
mod list_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrListCommand {
#[command(about = "Fetch all history events for the author with the given ID")]
AuthorHistory {
#[arg(
long,
help = "The Readarr ID of the author whose history you wish to fetch",
required = true
)]
author_id: i64,
},
#[command(about = "Trigger a manual search for releases of the author with the given ID")]
AuthorReleases {
#[arg(
long,
help = "The Readarr ID of the author whose releases you wish to search for",
required = true
)]
author_id: i64,
},
#[command(about = "List all authors in your Readarr library")]
Authors,
#[command(about = "List all items in the Readarr blocklist")]
Blocklist,
#[command(about = "List all editions for the book with the given ID")]
BookEditions {
#[arg(
long,
help = "The Readarr ID of the book whose editions you want to list",
required = true
)]
book_id: i64,
},
#[command(about = "List all book files for the book with the given ID")]
BookFiles {
#[arg(
long,
help = "The Readarr ID of the book whose book files you want to list",
required = true
)]
book_id: i64,
},
#[command(about = "Fetch all history events for the book with the given ID")]
BookHistory {
#[arg(
long,
help = "The Readarr ID of the author who wrote the book whose history you wish to fetch",
required = true
)]
author_id: i64,
#[arg(
long,
help = "The Readarr ID of the book whose history you wish to fetch",
required = true
)]
book_id: i64,
},
#[command(about = "List all books for the author with the given ID")]
Books {
#[arg(
long,
help = "The Readarr ID of the author whose books you want to list",
required = true
)]
author_id: i64,
},
#[command(about = "List disk space details for all provisioned root folders in Readarr")]
DiskSpace,
#[command(about = "List all active downloads in Readarr")]
Downloads {
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
count: u64,
},
#[command(about = "Fetch all Readarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
events: u64,
},
#[command(about = "List all Readarr indexers")]
Indexers,
#[command(about = "Fetch Readarr logs")]
Logs {
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
events: u64,
#[arg(
long,
help = "Output the logs in the same format as they appear in the log files"
)]
output_in_log_format: bool,
},
#[command(about = "List all Readarr metadata profiles")]
MetadataProfiles,
#[command(about = "List all Readarr quality profiles")]
QualityProfiles,
#[command(about = "List all queued events")]
QueuedEvents,
#[command(about = "Trigger a manual search for releases of the book with the given ID")]
Releases {
#[arg(
long,
help = "The Readarr ID of the book whose releases you wish to search for",
required = true
)]
book_id: i64,
},
#[command(about = "List all root folders in Readarr")]
RootFolders,
#[command(about = "List all Readarr tags")]
Tags,
#[command(about = "List all Readarr tasks")]
Tasks,
#[command(about = "List all Readarr updates")]
Updates,
}
impl From<ReadarrListCommand> for Command {
fn from(value: ReadarrListCommand) -> Self {
Command::Readarr(ReadarrCommand::List(value))
}
}
pub(super) struct ReadarrListCommandHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrListCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrListCommand> for ReadarrListCommandHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrListCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrListCommandHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrListCommand::AuthorHistory { author_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetAuthorHistory(author_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::AuthorReleases { author_id } => {
println!("Searching for author releases. This may take a minute...");
let resp = self
.network
.handle_network_event(ReadarrEvent::GetAuthorReleases(author_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Authors => {
let resp = self
.network
.handle_network_event(ReadarrEvent::ListAuthors.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Blocklist => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetBlocklist.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::BookEditions { book_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetBookEditions(book_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::BookFiles { book_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetBookFiles(book_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::BookHistory { author_id, book_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetBookHistory(author_id, book_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Books { author_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetBooks(author_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::DiskSpace => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetDiskSpace.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Downloads { count } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::History { events } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetHistory(events).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Indexers => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Logs {
events,
output_in_log_format,
} => {
let logs = self
.network
.handle_network_event(ReadarrEvent::GetLogs(events).into())
.await?;
if output_in_log_format {
let log_lines = &self.app.lock().await.data.readarr_data.logs.items;
serde_json::to_string_pretty(log_lines)?
} else {
serde_json::to_string_pretty(&logs)?
}
}
ReadarrListCommand::MetadataProfiles => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetMetadataProfiles.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::QualityProfiles => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetQualityProfiles.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::QueuedEvents => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetQueuedEvents.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Releases { book_id } => {
println!("Searching for book releases. This may take a minute...");
let resp = self
.network
.handle_network_event(ReadarrEvent::GetBookReleases(book_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::RootFolders => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetRootFolders.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Tags => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetTags.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Tasks => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetTasks.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrListCommand::Updates => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetUpdates.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,958 +0,0 @@
#[cfg(test)]
mod tests {
use crate::Cli;
use crate::cli::{
Command,
readarr::{ReadarrCommand, list_command_handler::ReadarrListCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_readarr_list_command_from() {
let command = ReadarrListCommand::DiskSpace;
let result = Command::from(command.clone());
assert_eq!(result, Command::Readarr(ReadarrCommand::List(command)));
}
mod cli {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_list_commands_have_no_arg_requirements(
#[values(
"authors",
"blocklist",
"disk-space",
"indexers",
"metadata-profiles",
"quality-profiles",
"queued-events",
"root-folders",
"tags",
"tasks",
"updates"
)]
subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", subcommand]);
assert_ok!(&result);
}
#[test]
fn test_list_logs_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "logs", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"list",
"downloads",
"--count",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "history", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_author_history_requires_author_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "author-history"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_author_history_success() {
let expected_args = ReadarrListCommand::AuthorHistory { author_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"list",
"author-history",
"--author-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(author_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(author_command, expected_args);
}
#[test]
fn test_list_books_requires_author_id() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "books"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_book_editions_requires_book_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-editions"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_book_editions_success() {
let expected_args = ReadarrListCommand::BookEditions { book_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"list",
"book-editions",
"--book-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(book_editions_command))) =
result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(book_editions_command, expected_args);
}
#[test]
fn test_list_book_files_requires_book_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-files"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_book_files_success() {
let expected_args = ReadarrListCommand::BookFiles { book_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"list",
"book-files",
"--book-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(book_files_command))) =
result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(book_files_command, expected_args);
}
#[test]
fn test_list_book_history_requires_author_id_and_book_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-history"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_book_history_success() {
let expected_args = ReadarrListCommand::BookHistory {
author_id: 1,
book_id: 2,
};
let result = Cli::try_parse_from([
"managarr",
"readarr",
"list",
"book-history",
"--author-id",
"1",
"--book-id",
"2",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(book_history_command))) =
result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(book_history_command, expected_args);
}
#[test]
fn test_list_books_success() {
let expected_args = ReadarrListCommand::Books { author_id: 1 };
let result =
Cli::try_parse_from(["managarr", "readarr", "list", "books", "--author-id", "1"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(books_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(books_command, expected_args);
}
#[test]
fn test_list_releases_requires_book_id() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "releases"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_author_releases_requires_author_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "author-releases"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_author_releases_success() {
let expected_args = ReadarrListCommand::AuthorReleases { author_id: 3 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"list",
"author-releases",
"--author-id",
"3",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(author_releases_command))) =
result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(author_releases_command, expected_args);
}
#[test]
fn test_list_releases_success() {
let expected_args = ReadarrListCommand::Releases { book_id: 7 };
let result =
Cli::try_parse_from(["managarr", "readarr", "list", "releases", "--book-id", "7"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(releases_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(releases_command, expected_args);
}
#[test]
fn test_list_logs_default_values() {
let expected_args = ReadarrListCommand::Logs {
events: 500,
output_in_log_format: false,
};
let result = Cli::try_parse_from(["managarr", "readarr", "list", "logs"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(logs_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(logs_command, expected_args);
}
#[test]
fn test_list_downloads_default_values() {
let expected_args = ReadarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "readarr", "list", "downloads"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(downloads_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(downloads_command, expected_args);
}
#[test]
fn test_list_downloads_success() {
let expected_args = ReadarrListCommand::Downloads { count: 1000 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"list",
"downloads",
"--count",
"1000",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(downloads_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(downloads_command, expected_args);
}
#[test]
fn test_list_history_default_values() {
let expected_args = ReadarrListCommand::History { events: 500 };
let result = Cli::try_parse_from(["managarr", "readarr", "list", "history"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(history_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(history_command, expected_args);
}
#[test]
fn test_list_history_success() {
let expected_args = ReadarrListCommand::History { events: 1000 };
let result =
Cli::try_parse_from(["managarr", "readarr", "list", "history", "--events", "1000"]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::List(history_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(history_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use pretty_assertions::assert_str_eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
CliCommandHandler,
readarr::list_command_handler::{ReadarrListCommand, ReadarrListCommandHandler},
},
models::{HorizontallyScrollableText, Serdeable, readarr_models::ReadarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
};
#[tokio::test]
async fn test_handle_list_authors_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::ListAuthors.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_authors_command = ReadarrListCommand::Authors;
let result =
ReadarrListCommandHandler::with(&app_arc, list_authors_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_blocklist_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_blocklist_command = ReadarrListCommand::Blocklist;
let result =
ReadarrListCommandHandler::with(&app_arc, list_blocklist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_author_history_command() {
let expected_author_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetAuthorHistory(expected_author_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_author_history_command = ReadarrListCommand::AuthorHistory { author_id: 1 };
let result =
ReadarrListCommandHandler::with(&app_arc, list_author_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_books_command() {
let expected_author_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetBooks(expected_author_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_books_command = ReadarrListCommand::Books { author_id: 1 };
let result = ReadarrListCommandHandler::with(&app_arc, list_books_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_book_editions_command() {
let expected_book_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetBookEditions(expected_book_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_book_editions_command = ReadarrListCommand::BookEditions { book_id: 1 };
let result =
ReadarrListCommandHandler::with(&app_arc, list_book_editions_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_book_files_command() {
let expected_book_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetBookFiles(expected_book_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_book_files_command = ReadarrListCommand::BookFiles { book_id: 1 };
let result =
ReadarrListCommandHandler::with(&app_arc, list_book_files_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_book_history_command() {
let expected_author_id = 1;
let expected_book_id = 2;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetBookHistory(expected_author_id, expected_book_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_book_history_command = ReadarrListCommand::BookHistory {
author_id: 1,
book_id: 2,
};
let result =
ReadarrListCommandHandler::with(&app_arc, list_book_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_disk_space_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetDiskSpace.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_disk_space_command = ReadarrListCommand::DiskSpace;
let result =
ReadarrListCommandHandler::with(&app_arc, list_disk_space_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_downloads_command() {
let expected_count = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = ReadarrListCommand::Downloads { count: 1000 };
let result =
ReadarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_history_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetHistory(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_history_command = ReadarrListCommand::History { events: 1000 };
let result =
ReadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_indexers_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetIndexers.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_indexers_command = ReadarrListCommand::Indexers;
let result =
ReadarrListCommandHandler::with(&app_arc, list_indexers_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_logs_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetLogs(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_logs_command = ReadarrListCommand::Logs {
events: 1000,
output_in_log_format: false,
};
let result = ReadarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_logs_command_output_in_log_format() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetLogs(1000).into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
app_arc
.lock()
.await
.data
.readarr_data
.logs
.set_items(vec![HorizontallyScrollableText::from("readarr log line")]);
let list_logs_command = ReadarrListCommand::Logs {
events: 1000,
output_in_log_format: true,
};
let result = ReadarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
assert_str_eq!(result.unwrap(), "[\n \"readarr log line\"\n]");
}
#[tokio::test]
async fn test_handle_list_metadata_profiles_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetMetadataProfiles.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_metadata_profiles_command = ReadarrListCommand::MetadataProfiles;
let result = ReadarrListCommandHandler::with(
&app_arc,
list_metadata_profiles_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_quality_profiles_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetQualityProfiles.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_quality_profiles_command = ReadarrListCommand::QualityProfiles;
let result =
ReadarrListCommandHandler::with(&app_arc, list_quality_profiles_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_queued_events_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetQueuedEvents.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_queued_events_command = ReadarrListCommand::QueuedEvents;
let result =
ReadarrListCommandHandler::with(&app_arc, list_queued_events_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_releases_command() {
let expected_book_id = 7;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetBookReleases(expected_book_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_releases_command = ReadarrListCommand::Releases { book_id: 7 };
let result =
ReadarrListCommandHandler::with(&app_arc, list_releases_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_author_releases_command() {
let expected_author_id = 3;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetAuthorReleases(expected_author_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_author_releases_command = ReadarrListCommand::AuthorReleases { author_id: 3 };
let result =
ReadarrListCommandHandler::with(&app_arc, list_author_releases_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_root_folders_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetRootFolders.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_root_folders_command = ReadarrListCommand::RootFolders;
let result =
ReadarrListCommandHandler::with(&app_arc, list_root_folders_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_tags_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetTags.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_tags_command = ReadarrListCommand::Tags;
let result = ReadarrListCommandHandler::with(&app_arc, list_tags_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_tasks_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetTasks.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_tasks_command = ReadarrListCommand::Tasks;
let result = ReadarrListCommandHandler::with(&app_arc, list_tasks_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_updates_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetUpdates.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_updates_command = ReadarrListCommand::Updates;
let result =
ReadarrListCommandHandler::with(&app_arc, list_updates_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
-285
View File
@@ -1,285 +0,0 @@
use std::sync::Arc;
use add_command_handler::{ReadarrAddCommand, ReadarrAddCommandHandler};
use anyhow::Result;
use clap::Subcommand;
use delete_command_handler::{ReadarrDeleteCommand, ReadarrDeleteCommandHandler};
use edit_command_handler::{ReadarrEditCommand, ReadarrEditCommandHandler};
use get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler};
use list_command_handler::{ReadarrListCommand, ReadarrListCommandHandler};
use refresh_command_handler::{ReadarrRefreshCommand, ReadarrRefreshCommandHandler};
use serde_json::json;
use tokio::sync::Mutex;
use trigger_automatic_search_command_handler::{
ReadarrTriggerAutomaticSearchCommand, ReadarrTriggerAutomaticSearchCommandHandler,
};
use super::{CliCommandHandler, Command};
use crate::models::readarr_models::ReadarrTaskName;
use crate::models::servarr_models::ReleaseDownloadBody;
use crate::network::readarr_network::ReadarrEvent;
use crate::{app::App, network::NetworkTrait};
mod add_command_handler;
mod delete_command_handler;
mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod refresh_command_handler;
mod trigger_automatic_search_command_handler;
#[cfg(test)]
#[path = "readarr_command_tests.rs"]
mod readarr_command_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrCommand {
#[command(
subcommand,
about = "Commands to add or create new resources within your Readarr instance"
)]
Add(ReadarrAddCommand),
#[command(
subcommand,
about = "Commands to delete resources from your Readarr instance"
)]
Delete(ReadarrDeleteCommand),
#[command(
subcommand,
about = "Commands to edit resources in your Readarr instance"
)]
Edit(ReadarrEditCommand),
#[command(
subcommand,
about = "Commands to fetch details of the resources in your Readarr instance"
)]
Get(ReadarrGetCommand),
#[command(
subcommand,
about = "Commands to list attributes from your Readarr instance"
)]
List(ReadarrListCommand),
#[command(
subcommand,
about = "Commands to refresh the data in your Readarr instance"
)]
Refresh(ReadarrRefreshCommand),
#[command(
subcommand,
about = "Commands to trigger automatic searches for releases of different resources in your Readarr instance"
)]
TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand),
#[command(about = "Clear the Readarr blocklist")]
ClearBlocklist,
#[command(about = "Manually download the given release")]
DownloadRelease {
#[arg(long, help = "The GUID of the release to download", required = true)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
},
#[command(about = "Mark the Readarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
long,
help = "The Readarr ID of the history item you wish to mark as 'failed'",
required = true
)]
history_item_id: i64,
},
#[command(about = "Search for a new author to add to Readarr")]
SearchNewAuthor {
#[arg(
long,
help = "The name of the author you want to search for",
required = true
)]
query: String,
},
#[command(about = "Start the specified Readarr task")]
StartTask {
#[arg(
long,
help = "The name of the task to trigger",
value_enum,
required = true
)]
task_name: ReadarrTaskName,
},
#[command(about = "Test all Readarr indexers")]
TestAllIndexers,
#[command(
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
)]
TestIndexer {
#[arg(long, help = "The ID of the indexer to test", required = true)]
indexer_id: i64,
},
#[command(
about = "Toggle monitoring for the specified author corresponding to the given author ID"
)]
ToggleAuthorMonitoring {
#[arg(
long,
help = "The Readarr ID of the author to toggle monitoring on",
required = true
)]
author_id: i64,
},
#[command(about = "Toggle monitoring for the specified book corresponding to the given book ID")]
ToggleBookMonitoring {
#[arg(
long,
help = "The Readarr ID of the book to toggle monitoring on",
required = true
)]
book_id: i64,
},
}
impl From<ReadarrCommand> for Command {
fn from(readarr_command: ReadarrCommand) -> Command {
Command::Readarr(readarr_command)
}
}
pub(super) struct ReadarrCliHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrCliHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrCommand::Add(add_command) => {
ReadarrAddCommandHandler::with(self.app, add_command, self.network)
.handle()
.await?
}
ReadarrCommand::Delete(delete_command) => {
ReadarrDeleteCommandHandler::with(self.app, delete_command, self.network)
.handle()
.await?
}
ReadarrCommand::Edit(edit_command) => {
ReadarrEditCommandHandler::with(self.app, edit_command, self.network)
.handle()
.await?
}
ReadarrCommand::Get(get_command) => {
ReadarrGetCommandHandler::with(self.app, get_command, self.network)
.handle()
.await?
}
ReadarrCommand::List(list_command) => {
ReadarrListCommandHandler::with(self.app, list_command, self.network)
.handle()
.await?
}
ReadarrCommand::Refresh(refresh_command) => {
ReadarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
.handle()
.await?
}
ReadarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
ReadarrTriggerAutomaticSearchCommandHandler::with(
self.app,
trigger_automatic_search_command,
self.network,
)
.handle()
.await?
}
ReadarrCommand::ClearBlocklist => {
self
.network
.handle_network_event(ReadarrEvent::GetBlocklist.into())
.await?;
let resp = self
.network
.handle_network_event(ReadarrEvent::ClearBlocklist.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::DownloadRelease { guid, indexer_id } => {
let params = ReleaseDownloadBody { guid, indexer_id };
let resp = self
.network
.handle_network_event(ReadarrEvent::DownloadRelease(params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
.handle_network_event(ReadarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
serde_json::to_string_pretty(
&json!({"message": "Readarr history item marked as 'failed'"}),
)?
}
ReadarrCommand::SearchNewAuthor { query } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::SearchNewAuthor(query).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::StartTask { task_name } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::StartTask(task_name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::TestAllIndexers => {
println!("Testing all Readarr indexers. This may take a minute...");
let resp = self
.network
.handle_network_event(ReadarrEvent::TestAllIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::TestIndexer { indexer_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::TestIndexer(indexer_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::ToggleAuthorMonitoring { author_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::ToggleAuthorMonitoring(author_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::ToggleBookMonitoring { book_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::ToggleBookMonitoring(book_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
-751
View File
@@ -1,751 +0,0 @@
#[cfg(test)]
mod tests {
use crate::cli::{
Command,
readarr::{ReadarrCommand, list_command_handler::ReadarrListCommand},
};
use pretty_assertions::assert_eq;
#[test]
fn test_readarr_command_from() {
let command = ReadarrCommand::List(ReadarrListCommand::Authors);
let result = Command::from(command.clone());
assert_eq!(result, Command::Readarr(command));
}
mod cli {
use clap::CommandFactory;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::Cli;
#[test]
fn test_readarr_command_requires_a_subcommand() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr"]);
assert_err!(&result);
}
#[test]
fn test_mark_history_item_as_failed_requires_history_item_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "mark-history-item-as-failed"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"download-release",
"--indexer-id",
"6",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"download-release",
"--guid",
"test-release-guid",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_release_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"download-release",
"--guid",
"test-release-guid",
"--indexer-id",
"6",
]);
assert_ok!(&result);
}
#[test]
fn test_mark_history_item_as_failed_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"mark-history-item-as-failed",
"--history-item-id",
"1234",
]);
assert_ok!(&result);
}
#[test]
fn test_search_new_author_requires_query() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "search-new-author"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_search_new_author_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"search-new-author",
"--query",
"test query",
]);
assert_ok!(&result);
}
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "start-task"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_start_task_task_name_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"start-task",
"--task-name",
"test",
]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_start_task_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"start-task",
"--task-name",
"check-health",
]);
assert_ok!(&result);
}
#[test]
fn test_test_indexer_requires_indexer_id() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "test-indexer"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_test_indexer_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"test-indexer",
"--indexer-id",
"8",
]);
assert_ok!(&result);
}
#[test]
fn test_toggle_author_monitoring_requires_author_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "toggle-author-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_author_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"toggle-author-monitoring",
"--author-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_toggle_book_monitoring_requires_book_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "toggle-book-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_commands_that_have_no_arg_requirements(
#[values("clear-blocklist", "test-all-indexers")] subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", subcommand]);
assert_ok!(&result);
}
#[test]
fn test_toggle_book_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"toggle-book-monitoring",
"--book-id",
"1",
]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::readarr::add_command_handler::ReadarrAddCommand;
use crate::cli::readarr::edit_command_handler::ReadarrEditCommand;
use crate::cli::readarr::get_command_handler::ReadarrGetCommand;
use crate::cli::readarr::refresh_command_handler::ReadarrRefreshCommand;
use crate::cli::readarr::trigger_automatic_search_command_handler::ReadarrTriggerAutomaticSearchCommand;
use crate::models::readarr_models::{
Author, BlocklistItem, BlocklistResponse, DeleteParams, ReadarrSerdeable, ReadarrTaskName,
};
use crate::models::servarr_models::{IndexerSettings, ReleaseDownloadBody};
use crate::{
app::App,
cli::{
CliCommandHandler,
readarr::{
ReadarrCliHandler, ReadarrCommand, delete_command_handler::ReadarrDeleteCommand,
list_command_handler::ReadarrListCommand,
},
},
models::Serdeable,
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
};
#[tokio::test]
async fn test_readarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = ReadarrCommand::Add(ReadarrAddCommand::Tag {
name: expected_tag_name,
});
let result = ReadarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = ReadarrCommand::Get(ReadarrGetCommand::SystemStatus);
let result = ReadarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
let expected_delete_author_params = DeleteParams {
id: 7,
delete_files: true,
add_import_list_exclusion: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::DeleteAuthor(expected_delete_author_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_author_command = ReadarrCommand::Delete(ReadarrDeleteCommand::Author {
author_id: 7,
delete_files_from_disk: true,
add_list_exclusion: false,
});
let result = ReadarrCliHandler::with(&app_arc, delete_author_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 26500,
minimum_age: 17,
retention: 43,
rss_sync_interval: 35,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::IndexerSettings(
IndexerSettings {
id: 9,
maximum_size: 31200,
minimum_age: 22,
retention: 58,
rss_sync_interval: 90,
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command =
ReadarrCommand::Edit(ReadarrEditCommand::AllIndexerSettings {
maximum_size: Some(26500),
minimum_age: Some(17),
retention: Some(43),
rss_sync_interval: Some(35),
});
let result = ReadarrCliHandler::with(
&app_arc,
edit_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::ListAuthors.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Authors(vec![
Author::default(),
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_authors_command = ReadarrCommand::List(ReadarrListCommand::Authors);
let result = ReadarrCliHandler::with(&app_arc, list_authors_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateAllAuthors.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_all_authors_command = ReadarrCommand::Refresh(ReadarrRefreshCommand::AllAuthors);
let result =
ReadarrCliHandler::with(&app_arc, refresh_all_authors_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{
let expected_author_id = 7;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::TriggerAutomaticAuthorSearch(expected_author_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
ReadarrCommand::TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand::Author {
author_id: expected_author_id,
});
let result = ReadarrCliHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_mark_history_item_as_failed_command() {
let expected_history_item_id = 1234i64;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let mark_history_item_as_failed_command = ReadarrCommand::MarkHistoryItemAsFailed {
history_item_id: expected_history_item_id,
};
let result = ReadarrCliHandler::with(
&app_arc,
mark_history_item_as_failed_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_download_release_command() {
let expected_release_download_body = ReleaseDownloadBody {
guid: "test-release-guid".to_owned(),
indexer_id: 6,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::DownloadRelease(expected_release_download_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_release_command = ReadarrCommand::DownloadRelease {
guid: "test-release-guid".to_owned(),
indexer_id: 6,
};
let result = ReadarrCliHandler::with(&app_arc, download_release_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_search_new_author_command() {
let expected_query = "test author".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::SearchNewAuthor(expected_query.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let search_new_author_command = ReadarrCommand::SearchNewAuthor {
query: expected_query,
};
let result = ReadarrCliHandler::with(&app_arc, search_new_author_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_start_task_command() {
let expected_task_name = ReadarrTaskName::CheckHealth;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::StartTask(expected_task_name).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let start_task_command = ReadarrCommand::StartTask {
task_name: ReadarrTaskName::CheckHealth,
};
let result = ReadarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_test_indexer_command() {
let expected_indexer_id = 8;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::TestIndexer(expected_indexer_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_indexer_command = ReadarrCommand::TestIndexer { indexer_id: 8 };
let result = ReadarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_test_all_indexers_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::TestAllIndexers.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_all_indexers_command = ReadarrCommand::TestAllIndexers;
let result = ReadarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_toggle_author_monitoring_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::ToggleAuthorMonitoring(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_author_monitoring_command =
ReadarrCommand::ToggleAuthorMonitoring { author_id: 1 };
let result = ReadarrCliHandler::with(
&app_arc,
toggle_author_monitoring_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_toggle_book_monitoring_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::ToggleBookMonitoring(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_book_monitoring_command = ReadarrCommand::ToggleBookMonitoring { book_id: 1 };
let result =
ReadarrCliHandler::with(&app_arc, toggle_book_monitoring_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_clear_blocklist_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::BlocklistResponse(
BlocklistResponse {
records: vec![BlocklistItem::default()],
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::ClearBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let clear_blocklist_command = ReadarrCommand::ClearBlocklist;
let result = ReadarrCliHandler::with(&app_arc, clear_blocklist_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -1,90 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, readarr_network::ReadarrEvent},
};
use super::ReadarrCommand;
#[cfg(test)]
#[path = "refresh_command_handler_tests.rs"]
mod refresh_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrRefreshCommand {
#[command(about = "Refresh all author data for all authors in your Readarr library")]
AllAuthors,
#[command(about = "Refresh author data and scan disk for the author with the given ID")]
Author {
#[arg(
long,
help = "The ID of the author to refresh information on and to scan the disk for",
required = true
)]
author_id: i64,
},
#[command(about = "Refresh all downloads in Readarr")]
Downloads,
}
impl From<ReadarrRefreshCommand> for Command {
fn from(value: ReadarrRefreshCommand) -> Self {
Command::Readarr(ReadarrCommand::Refresh(value))
}
}
pub(super) struct ReadarrRefreshCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrRefreshCommand>
for ReadarrRefreshCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrRefreshCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrRefreshCommand::AllAuthors => {
let resp = self
.network
.handle_network_event(ReadarrEvent::UpdateAllAuthors.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrRefreshCommand::Author { author_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::UpdateAndScanAuthor(author_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrRefreshCommand::Downloads => {
let resp = self
.network
.handle_network_event(ReadarrEvent::UpdateDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,162 +0,0 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::Cli;
use crate::cli::{
Command,
readarr::{ReadarrCommand, refresh_command_handler::ReadarrRefreshCommand},
};
use clap::CommandFactory;
#[test]
fn test_readarr_refresh_command_from() {
let command = ReadarrRefreshCommand::Author { author_id: 1 };
let result = Command::from(command.clone());
assert_eq!(result, Command::Readarr(ReadarrCommand::Refresh(command)));
}
mod cli {
use super::*;
use clap::Parser;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_refresh_all_authors_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "all-authors"]);
assert_ok!(&result);
}
#[test]
fn test_refresh_downloads_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "downloads"]);
assert_ok!(&result);
}
#[test]
fn test_refresh_author_requires_author_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "author"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_refresh_author_with_author_id() {
let expected_args = ReadarrRefreshCommand::Author { author_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"refresh",
"author",
"--author-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::Refresh(refresh_command))) =
result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(refresh_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::readarr::refresh_command_handler::{
ReadarrRefreshCommand, ReadarrRefreshCommandHandler,
};
use crate::{app::App, cli::CliCommandHandler};
use crate::{
models::{Serdeable, readarr_models::ReadarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
};
#[tokio::test]
async fn test_handle_refresh_all_authors_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateAllAuthors.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = ReadarrRefreshCommand::AllAuthors;
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_refresh_author_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::UpdateAndScanAuthor(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = ReadarrRefreshCommand::Author { author_id: 1 };
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_refresh_downloads_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateDownloads.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = ReadarrRefreshCommand::Downloads;
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -1,88 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, readarr_network::ReadarrEvent},
};
use super::ReadarrCommand;
#[cfg(test)]
#[path = "trigger_automatic_search_command_handler_tests.rs"]
mod trigger_automatic_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrTriggerAutomaticSearchCommand {
#[command(about = "Trigger an automatic search for the author with the specified ID")]
Author {
#[arg(
long,
help = "The ID of the author you want to trigger an automatic search for",
required = true
)]
author_id: i64,
},
#[command(about = "Trigger an automatic search for the book with the specified ID")]
Book {
#[arg(
long,
help = "The ID of the book you want to trigger an automatic search for",
required = true
)]
book_id: i64,
},
}
impl From<ReadarrTriggerAutomaticSearchCommand> for Command {
fn from(value: ReadarrTriggerAutomaticSearchCommand) -> Self {
Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(value))
}
}
pub(super) struct ReadarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrTriggerAutomaticSearchCommand>
for ReadarrTriggerAutomaticSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrTriggerAutomaticSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrTriggerAutomaticSearchCommand::Author { author_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::TriggerAutomaticAuthorSearch(author_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrTriggerAutomaticSearchCommand::Book { book_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::TriggerAutomaticBookSearch(book_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -1,186 +0,0 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::Cli;
use crate::cli::{
Command,
readarr::{
ReadarrCommand,
trigger_automatic_search_command_handler::ReadarrTriggerAutomaticSearchCommand,
},
};
use clap::CommandFactory;
#[test]
fn test_readarr_trigger_automatic_search_command_from() {
let command = ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(command))
);
}
mod cli {
use super::*;
use clap::Parser;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_trigger_automatic_author_search_requires_author_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"trigger-automatic-search",
"author",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_author_search_with_author_id() {
let expected_args = ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"trigger-automatic-search",
"author",
"--author-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(
trigger_automatic_search_command,
))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(trigger_automatic_search_command, expected_args);
}
#[test]
fn test_trigger_automatic_book_search_requires_book_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"readarr",
"trigger-automatic-search",
"book",
]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_book_search_with_book_id() {
let expected_args = ReadarrTriggerAutomaticSearchCommand::Book { book_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"readarr",
"trigger-automatic-search",
"book",
"--book-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(
trigger_automatic_search_command,
))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(trigger_automatic_search_command, expected_args);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::readarr::trigger_automatic_search_command_handler::{
ReadarrTriggerAutomaticSearchCommand, ReadarrTriggerAutomaticSearchCommandHandler,
};
use crate::{app::App, cli::CliCommandHandler};
use crate::{
models::{Serdeable, readarr_models::ReadarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
};
#[tokio::test]
async fn test_handle_trigger_automatic_author_search_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::TriggerAutomaticAuthorSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
let result = ReadarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_trigger_automatic_book_search_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::TriggerAutomaticBookSearch(1).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
ReadarrTriggerAutomaticSearchCommand::Book { book_id: 1 };
let result = ReadarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
+2 -2
View File
@@ -137,12 +137,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan
root_folder_path,
quality_profile_id,
language_profile_id,
series_type,
series_type: series_type.to_string(),
season_folder: !disable_season_folders,
tags,
tag_input_string: None,
add_options: AddSeriesOptions {
monitor,
monitor: monitor.to_string(),
search_for_cutoff_unmet_episodes: !no_search_for_series,
search_for_missing_episodes: !no_search_for_series,
},
+2 -2
View File
@@ -517,13 +517,13 @@ mod tests {
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
language_profile_id: 1,
series_type: SeriesType::Anime,
series_type: "anime".to_owned(),
monitored: false,
tags: vec![1, 2],
tag_input_string: None,
season_folder: false,
add_options: AddSeriesOptions {
monitor: SeriesMonitor::Future,
monitor: "future".to_owned(),
search_for_cutoff_unmet_episodes: false,
search_for_missing_episodes: false,
},
+2 -2
View File
@@ -9,8 +9,8 @@ use crate::{
cli::{CliCommandHandler, Command, mutex_flags_or_option},
models::{
Serdeable,
servarr_models::{EditIndexerParams, IndexerSettings},
sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable},
servarr_models::EditIndexerParams,
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
+15 -15
View File
@@ -622,8 +622,8 @@ mod tests {
},
models::{
Serdeable,
servarr_models::{EditIndexerParams, IndexerSettings},
sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable},
servarr_models::EditIndexerParams,
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
@@ -632,10 +632,10 @@ mod tests {
async fn test_handle_edit_all_indexer_settings_command() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 12345,
minimum_age: 13,
retention: 21,
rss_sync_interval: 60,
maximum_size: 1,
minimum_age: 1,
retention: 1,
rss_sync_interval: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
@@ -647,11 +647,11 @@ mod tests {
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
IndexerSettings {
id: 9,
maximum_size: 22222,
minimum_age: 33,
retention: 47,
rss_sync_interval: 90,
id: 1,
maximum_size: 2,
minimum_age: 2,
retention: 2,
rss_sync_interval: 2,
},
)))
});
@@ -668,10 +668,10 @@ mod tests {
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings {
maximum_size: Some(12345),
minimum_age: Some(13),
retention: Some(21),
rss_sync_interval: Some(60),
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
};
let result = SonarrEditCommandHandler::with(
+1 -1
View File
@@ -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)?
}
+1 -1
View File
@@ -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(|_| {
+10 -32
View File
@@ -2,18 +2,16 @@ use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use serde_json::json;
use tokio::sync::Mutex;
use super::SonarrCommand;
use crate::models::Serdeable;
use crate::models::sonarr_models::{SonarrRelease, SonarrSerdeable};
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "manual_search_command_handler_tests.rs"]
mod manual_search_command_handler_tests;
@@ -30,7 +28,7 @@ pub enum SonarrManualSearchCommand {
episode_id: i64,
},
#[command(
about = "Trigger a manual search of full-season releases (full_season: true) for the given season corresponding to the series with the given ID"
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID.\nNote that when downloading a season release, ensure that the release includes 'fullSeason: true', otherwise you'll run into issues"
)]
Season {
#[arg(
@@ -75,42 +73,22 @@ 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...");
match self
let resp = self
.network
.handle_network_event(SonarrEvent::GetEpisodeReleases(episode_id).into())
.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"}))?,
}
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrManualSearchCommand::Season {
series_id,
season_number,
} => {
println!("Searching for season releases. This may take a minute...");
match self
let resp = self
.network
.handle_network_event(SonarrEvent::GetSeasonReleases(series_id, season_number).into())
.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": "Failed to parse response"}))?,
}
.handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
@@ -108,13 +108,9 @@ mod tests {
use std::sync::Arc;
use mockall::predicate::eq;
use pretty_assertions::assert_str_eq;
use serde_json::json;
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::{
@@ -138,13 +134,9 @@ mod tests {
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![
torrent_release(),
SonarrRelease {
full_season: true,
..usenet_release()
},
])))
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 };
@@ -158,35 +150,23 @@ 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::Releases(vec![
torrent_release(),
SonarrRelease {
full_season: true,
..usenet_release()
},
])))
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_season_search_command = SonarrManualSearchCommand::Season {
@@ -203,10 +183,6 @@ mod tests {
.await;
assert_ok!(&result);
assert_str_eq!(
result.unwrap(),
serde_json::to_string_pretty(&[expected_release]).unwrap()
);
}
}
}
+2 -3
View File
@@ -10,7 +10,6 @@ use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler};
use list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler};
use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler};
use serde_json::json;
use tokio::sync::Mutex;
use trigger_automatic_search_command_handler::{
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
@@ -252,7 +251,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
.network
.handle_network_event(SonarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
serde_json::to_string_pretty(&json!({"message": "Sonarr history item marked as 'failed'"}))?
"Sonarr history item marked as 'failed'".to_owned()
}
SonarrCommand::SearchNewSeries { query } => {
let resp = self
@@ -297,7 +296,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)?
+3 -4
View File
@@ -266,10 +266,9 @@ mod tests {
},
models::{
Serdeable,
servarr_models::IndexerSettings,
sonarr_models::{
BlocklistItem, BlocklistResponse, Series, SonarrReleaseDownloadBody, SonarrSerdeable,
SonarrTaskName,
BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody,
SonarrSerdeable, SonarrTaskName,
},
},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
@@ -755,7 +754,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(|_| {
Loaded 100 of 1130 files, more files were not shown because too many files have changed in this diff. Show more