Compare commits
228 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6006c9d0e8 | |||
|
|
c93543186a | ||
|
|
e1b74d7a36 | ||
| 6375bc3413 | |||
|
|
9c99e0d2ef | ||
|
|
518ccaadc8 | ||
| a766b395c1 | |||
| 1746869b45 | |||
|
|
533366b90b | ||
| 5c68df7246 | |||
| cc26d9a655 | |||
| 02a1303557 | |||
|
|
817ffcf2b5 | ||
| 4a8b7eb837 | |||
| fcf81ad7d9 | |||
| 89a1b0dec7 | |||
| face51ae0c | |||
| 5ca6757fb0 | |||
| 4f4633c6b3 | |||
| 17c70dfef2 | |||
| 92daaebf9d | |||
| 0c70191687 | |||
|
|
e2a5f2f1b5 | ||
| 1893abd773 | |||
| 41bb08418f | |||
| cf1794145c | |||
| 7d9a25e599 | |||
|
|
cd2175b5ad | ||
| 98ff5184e1 | |||
|
|
da785355d6 | ||
| 4cabd5f0d0 | |||
| a41f03c6b2 | |||
| cde86cf9fd | |||
| 8d4981f10f | |||
| 93d023c54a | |||
| bae450f23e | |||
| 42339e65d4 | |||
| 0bb839c0a0 | |||
|
|
6381d7b742 | ||
| 2efbdad4f0 | |||
| 30bf0c22fa | |||
| 169d6c7364 | |||
| e1fb5f570e | |||
| 92362a5d8c | |||
| 7c88901185 | |||
| 5bce9b240e | |||
| e675168798 | |||
| 8aa88d7343 | |||
|
|
9e6879c0f2 | ||
| eb856e28d7 | |||
| 866b0c7537 | |||
| 3ef5c1911d | |||
| 80787d1187 | |||
| ad0b3989ed | |||
| c7a0e33485 | |||
| ee312a21eb | |||
| 3af22cceac | |||
| c29e2ca9ae | |||
| 06c9baf8df | |||
| 97dc5054e9 | |||
| d43862a3a7 | |||
| 1dd4cd74c3 | |||
| 4f86cce497 | |||
| 3968983002 | |||
| 4c7e8f0cf6 | |||
| 1e3141e4ee | |||
| 45542cd3a9 | |||
| da3bb795b7 | |||
| 53a59cdb4c | |||
| 8125bd5ae0 | |||
| 5ba3f2b1ba | |||
| c98828aec7 | |||
| 5ed278ec9c | |||
| c8a2fea9cd | |||
| cac54c5447 | |||
| 374819b4f3 | |||
| 4d92c350de | |||
| 3be9321df6 | |||
| 746064c430 | |||
| ffc00691cb | |||
| 1b5979c36c | |||
| 5ed3372ae2 | |||
| 8002a5aa1e | |||
| 896c50909a | |||
| cea4632a22 | |||
| 7fdec15ba9 | |||
| eb06787bb2 | |||
| c3577a0724 | |||
| 8864e2c867 | |||
| 581975b941 | |||
| b8e4deb80f | |||
| 40bb22ef7c | |||
| 74e9ea17ac | |||
| a11bce603d | |||
| c754275af3 | |||
| 3497a54c39 | |||
| 8807adea83 | |||
| 6896fcc134 | |||
| 68830a8789 | |||
| 2dce587ea8 | |||
| 9403bdcbcb | |||
| aa13735533 | |||
| 33db3efacf | |||
| 8df74585bc | |||
| 16ca8841a1 | |||
| 22fbe025d9 | |||
| c54bd2bab0 | |||
| 539ad75fe6 | |||
| 9476caa392 | |||
| df3cf70682 | |||
| a881d1f33a | |||
| d96316577a | |||
| eefe6392df | |||
| 1cc95e2cd1 | |||
| c5328917de | |||
| 208acafc73 | |||
| 57eced64c0 | |||
| ce701c1ab7 | |||
| b24e3bf9db | |||
| 1227796e78 | |||
| bb1c08277e | |||
| 16538a3158 | |||
| f4c647342b | |||
| 72cb334b6a | |||
| 1a65a7f3e7 | |||
| 6a0049eb8f | |||
| d2e3750de6 | |||
| 4cdad182ef | |||
| 4ed1e99a15 | |||
| 71870d9396 | |||
| fa4ec709c0 | |||
| d7d223400e | |||
| 34157ef32f | |||
| f5631376af | |||
| df1eea22ab | |||
| 86d93377ac | |||
| 5872a6ba72 | |||
| 6da1ae93ef | |||
| b8c60bf59a | |||
| bd2d2875a5 | |||
| 9d782af020 | |||
| a711c3d16c | |||
| 268cc13d27 | |||
| a8328d3636 | |||
| d82a7f7674 | |||
| 5e63c34a9f | |||
| 540db5993b | |||
| 16bf06426f | |||
| cc02832512 | |||
| 2876913f48 | |||
| 6b64b5ecc4 | |||
| 9ceb55a314 | |||
| 7870bb4b5b | |||
| 4fc2d3c94b | |||
| a012945df2 | |||
| f094cf5ad3 | |||
| d8979221c8 | |||
| aaa4e67f43 | |||
|
|
fff38704ab | ||
| d5e6d64d0f | |||
| 003f319385 | |||
| e14b7072c6 | |||
| 1fe95d057b | |||
| 6dffc90e92 | |||
| 295cd56a1f | |||
| 214c89e8b5 | |||
| 29047c3007 | |||
| a8f3bed402 | |||
| 1ca9265a2a | |||
| 60d61b9e31 | |||
| b6f5b9d08c | |||
| 28f7bc6a4c | |||
| 5b42129f55 | |||
| 4f06b2b947 | |||
| 0f98050a12 | |||
| 14839642dc | |||
| eccc1a2df1 | |||
| 9df929a8e3 | |||
| 1e008f9778 | |||
|
|
fa811da5c2 | ||
| 48ad17c6f1 | |||
| 3cd15f34cd | |||
| 53ca14e64d | |||
| 0d8803d35d | |||
| 8c90221a81 | |||
| a708f71d57 | |||
| 2a13f74a2b | |||
| 2a97c49a8e | |||
| 8c155ce656 | |||
|
|
5245ba6d98 | ||
|
|
f9789ecc9b | ||
| 9936ce1ab5 | |||
| 650c9783a6 | |||
| b253a389eb | |||
| 5023fbd3d1 | |||
| fdb08fbd34 | |||
| b125d3341a | |||
|
|
f73e3a4817 | ||
| 1ff31b1bd9 | |||
| ce4cbd8652 | |||
| 346d95f8ec | |||
| 85ea05e3c8 | |||
| 93d78701ce | |||
| 8d7cb63c7a | |||
| c8c7d00517 | |||
|
|
9402ad3f3b | ||
|
|
ea9a9070ce | ||
| a0fe51c57b | |||
|
|
9326428141 | ||
|
|
c1da8592b4 | ||
|
|
aa43219c29 | ||
| f6f477b124 | |||
|
|
76f22e7434 | ||
|
|
28aad8cd14 | ||
| 97c8f8fc49 | |||
| 9da4ebfe11 | |||
| 9961fe2f82 | |||
| 61ce0468c6 | |||
|
|
29071b11da | ||
|
|
f2129ba321 | ||
|
|
945dc744bc | ||
| 9ee41b7af5 | |||
|
|
a1d6df6b85 | ||
|
|
154f9c624a | ||
| f310db6722 | |||
| 1433ca24cc | |||
| fa8aefe049 | |||
| 88f16f63c2 |
@@ -0,0 +1,10 @@
|
||||
[tool.commitizen]
|
||||
name = "cz_conventional_commits"
|
||||
tag_format = "v$version"
|
||||
version_scheme = "semver"
|
||||
version_provider = "cargo"
|
||||
update_changelog_on_bump = true
|
||||
major_version_zero = true
|
||||
|
||||
[tool.commitizen.hooks]
|
||||
pre-commit = "git add Cargo.toml Cargo.lock"
|
||||
@@ -0,0 +1 @@
|
||||
github: Dark-Alex-17
|
||||
+117
-16
@@ -1,32 +1,133 @@
|
||||
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
|
||||
# Thanks to joshka for permission to use this template!
|
||||
|
||||
name: Create Release PR and publish to crates.io
|
||||
name: Create release
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump_type:
|
||||
description: "Specify the type of version bump"
|
||||
required: true
|
||||
default: "patch"
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
jobs:
|
||||
release-plz:
|
||||
# see https://release-plz.ieni.dev/docs/github
|
||||
# for more information
|
||||
name: Release-plz
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Configure SSH for Git
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.RELEASE_BOT_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install Commitizen
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install commitizen
|
||||
npm install -g conventional-changelog-cli
|
||||
|
||||
- name: Configure Git user
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump version with Commitizen
|
||||
run: |
|
||||
cz bump --yes --increment ${{ github.event.inputs.bump_type }}
|
||||
|
||||
- name: Amend commit message to include '[skip ci]'
|
||||
run: |
|
||||
git commit --amend --no-edit -m "$(git log -1 --pretty=%B) [skip ci]"
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Update the Cargo.lock
|
||||
run: |
|
||||
cargo update
|
||||
git add Cargo.lock
|
||||
git commit -m "chore: Bump the version in Cargo.lock"
|
||||
|
||||
- name: Get the new version tag
|
||||
id: version
|
||||
run: |
|
||||
NEW_TAG=$(cz version --project)
|
||||
echo "New version: $NEW_TAG"
|
||||
echo "version=$NEW_TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: Get the previous version tag
|
||||
id: prev_version
|
||||
run: |
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 ${GITHUB_SHA}^)
|
||||
echo "Previous tag: $PREV_TAG"
|
||||
echo "prev_version=$PREV_TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate changelog for the version bump
|
||||
id: changelog
|
||||
run: |
|
||||
changelog=$(conventional-changelog -p angular -i CHANGELOG.md -s --from ${{ env.prev_version }} --to ${{ env.version }})
|
||||
echo "$changelog" > changelog.md
|
||||
echo "changelog_body=$(cat changelog.md)" >> $GITHUB_ENV
|
||||
|
||||
- name: Create a GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ env.version }}
|
||||
name: "Release v${{ env.version }}"
|
||||
body: ${{ env.changelog_body }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Push changes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git push origin --follow-tags
|
||||
|
||||
release-crate:
|
||||
needs: bump
|
||||
name: Release Crate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if actor is repository owner
|
||||
if: ${{ github.actor != github.repository_owner }}
|
||||
run: |
|
||||
echo "You are not authorized to run this workflow."
|
||||
exit 1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Ensure repository is up-to-date
|
||||
run: |
|
||||
git fetch --all
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Run release-plz
|
||||
uses: MarcoIeni/release-plz-action@v0.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
repos:
|
||||
- hooks:
|
||||
- id: commitizen
|
||||
- id: commitizen-branch
|
||||
stages:
|
||||
- pre-push
|
||||
repo: https://github.com/commitizen-tools/commitizen
|
||||
rev: v3.30.0
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
# Changelog
|
||||
|
||||
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.3.4 (2024-11-26)
|
||||
|
||||
## v0.3.3 (2024-11-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- **ci**: Properly prefix version tags with 'v' [skip ci]
|
||||
- **ci**: Bump the version in the Cargo.lock file and commit it as well when releasing [skip ci]
|
||||
|
||||
## v0.3.2 (2024-11-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- **ci**: Updated the Cargo.lock file [skip ci]
|
||||
- **ci**: Use a different GitHub action to release the crate to Crates.io [skip ci]
|
||||
- **ci**: Don't manually push the tags and let Commitizen do it [skip ci]
|
||||
|
||||
## v0.3.1 (2024-11-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- **ci**: Don't manually push the tags and let Commitizen do it [skip ci]
|
||||
- **ci**: Fixed a typo in the version creation on GitHub [skip ci]
|
||||
|
||||
## v0.3.0 (2024-11-26)
|
||||
|
||||
### Feat
|
||||
|
||||
- **cli**: Support for editing a sonarr series
|
||||
- **models**: Added the ActiveSonarrBlocks for editing a series
|
||||
- **network**: Support for editing a series in Sonarr
|
||||
- **models**: Created the EditSeriesModal
|
||||
- **cli**: Support for editing Sonarr indexers
|
||||
- **network**: Support for editing a sonarr indexer
|
||||
- **cli**: Support for deleting an episode file from disk
|
||||
- **network**: Support for deleting an episode file from disk in Sonarr
|
||||
- **cli**: Support for editing all indexer settings in Sonarr
|
||||
- **models**: Added the ActiveSonarrBlocks for editing all indexer settings
|
||||
- **network**: Support for editing all sonarr indexer settings
|
||||
- **cli**: Support for searching for new series to add to Sonarr
|
||||
- **network**: Support for searching for new series
|
||||
- **cli**: Support for adding a series to Sonarr
|
||||
- **cli**: Support for adding a series to Sonarr
|
||||
- **network**: Support for adding a new series to Sonarr
|
||||
- **cli**: Support for fetching all sonarr language profiles
|
||||
- **network**: Support for fetching all Sonarr language profiles
|
||||
- **cli**: Support for deleting a series from Sonarr
|
||||
- **network**: Support for deleting a series from Sonarr
|
||||
- **cli**: Support for downloading an episode release in Sonarr
|
||||
- **cli**: Support for downloading a season release in Sonarr
|
||||
- **cli**: Support for downloading a Series release in Sonarr
|
||||
- **network**: Support for downloading releases from Sonarr
|
||||
- **cli**: Support for refreshing Sonarr downloads
|
||||
- **network**: Support for updating Sonarr downloads
|
||||
- **cli**: Support for refreshing a specific series in Sonarr
|
||||
- **network**: Support for updating and scanning a series in Sonarr
|
||||
- **cli**: Support for refreshing all Sonarr series data
|
||||
- **network**: Support for updating all series in Sonarr
|
||||
- **cli**: Support for triggering an automatic episode search in Sonarr
|
||||
- **cli**: Support for triggering an automatic season search in Sonarr
|
||||
- **cli**: Support for triggering an automatic series search in Sonarr
|
||||
- **network**: Support for triggering an automatic episode search in Sonarr
|
||||
- **network**: Support for triggering an automatic season search in Sonarr
|
||||
- **network**: Support for triggering an automatic series search in Sonarr
|
||||
- **cli**: Support for testing all Sonarr indexers at once
|
||||
- **network**: Support for testing all Sonarr indexers at once
|
||||
- **cli**: Support for testing an individual Sonarr indexer
|
||||
- **network**: Added the ability to test an individual indexer in Sonarr
|
||||
- **cli**: Support for starting a Sonarr task
|
||||
- **network**: Support for starting a Sonarr task
|
||||
- **cli**: Support for listing Sonarr updates
|
||||
- **network**: Support for fetching Sonarr updates
|
||||
- **cli**: Support for listing all Sonarr tasks
|
||||
- **network**: Support for fetching all Sonarr tasks
|
||||
- **cli**: Support for marking a Sonarr history item as 'failed'
|
||||
- **network**: Support for marking a Sonarr history item as failed
|
||||
- **cli**: Support for listing the available disk space for all provisioned root folders in both Radarr and Sonarr
|
||||
- **network**: Support for listing disk space on a Sonarr instance
|
||||
- **cli**: Support for listing all Sonarr tags
|
||||
- **cli**: Support for adding a root folder to Sonarr
|
||||
- **cli**: CLI support for adding a tag to Sonarr
|
||||
- **network**: Support for fetching and listing all Sonarr tags
|
||||
- **network**: Support for deleting tags from Sonarr
|
||||
- **network**: Support for adding tags to Sonarr
|
||||
- **network**: Support for adding a root folder to Sonarr
|
||||
- **cli**: Support for deleting a root folder from Sonarr
|
||||
- **network**: Support for deleting a Sonarr root folder
|
||||
- **cli**: Support for fetching all Sonarr root folders
|
||||
- **network**: Support for fetching all Sonarr root folders
|
||||
- **cli**: Support for deleting a Sonarr indexer
|
||||
- **network**: Support for deleting an indexer from Sonarr
|
||||
- **cli**: Support for deleting a download from Sonarr
|
||||
- **network**: Support for deleting a download from Sonarr
|
||||
- **cli**: Support for fetching episode history events from Sonarr
|
||||
- **network**: Support for fetching episode history
|
||||
- **cli**: Added a spinner to the CLI for long running commands like fetching releases
|
||||
- **cli**: Support for fetching history for a given series ID
|
||||
- **network**: Support for fetching Sonarr series history for a given series ID
|
||||
- **cli**: Support for fetching all Sonarr history events
|
||||
- **network**: Support to fetch all Sonarr history events
|
||||
- **models**: Added an additional History tab to the mocked tabs for viewing all Sonarr history at once
|
||||
- **models**: Stubbed out the necessary ActiveSonarrBlocks for the UI mockup
|
||||
- **cli**: Added support for manually searching for episode releases in Sonarr
|
||||
- **network**: Added support for fetching episode releases in Sonarr
|
||||
- **cli**: Added CLI support for fetching series details in Sonarr
|
||||
- **network**: Added support for fetching series details for a given series ID in Sonarr
|
||||
- **cli**: Added support for manually searching for season releases for Sonarr
|
||||
- **network**: Added support for fetching season releases for Sonarr
|
||||
- **cli**: Added support for listing Sonarr queued events
|
||||
- **network**: Added support for fetching Sonarr queued events
|
||||
- **cli**: Added CLI support for fetching all indexer settings for Sonarr
|
||||
- **network**: Added netwwork support for fetching all indexer settings for Sonarr
|
||||
- **cli**: Added Sonarr support for fetching host and security configs
|
||||
- **network**: Added network support for fetching host and security configs from Sonarr
|
||||
- **cli**: Added CLI support for listing Sonarr indexers
|
||||
- **network**: Added the GetIndexers network call for Sonarr
|
||||
- **cli**: Added sonarr support for listing downloads, listing quality profiles, and fetching detailed information about an episode
|
||||
- **network**: Added get quality profiles and get episode details events for Sonarr
|
||||
- **cli**: Sonarr CLI support for fetching all episodes for a given series
|
||||
- **sonarr_network**: Added support for fetching episodes for a specified series to the network events
|
||||
- **models**: Added the Episode model to Sonarr models
|
||||
- **models**: Created the StatefulTree struct for displaying seasons and episodes (and any other structured data) for the UI.
|
||||
- **sonarr**: Added CLI support for listing Sonarr logs
|
||||
- **sonarr**: Added the ability to fetch Sonarr logs
|
||||
- **sonarr**: Added blocklist commands (List, Clear, Delete)
|
||||
- Added initial Sonarr CLI support and the initial network handler setup for the TUI
|
||||
- Added a new command to the main managarr CLI: tail-logs, to enable users to tail the Managarr logs without needing to know where the log file itself is located
|
||||
|
||||
### Fix
|
||||
|
||||
- Reverted to old version to fix release [skip ci]
|
||||
- **minimal-versions**: Addressed concerns with the minimal-versions CI checks
|
||||
- **lint**: Addressed linter complaints
|
||||
- **cli**: Corrected some copy/paste typos
|
||||
- **network**: Force sonarr to save edits to indexers
|
||||
- **network**: Made the overview field nullable in the Sonarr series model
|
||||
- **network**: Added filtering for full seasons specifically in the UI when performing a manual full season search and added a message to the CLI that noes to only try to download a full season if that release includes 'fullSeason: true'
|
||||
- **network**: Not all Sonarr tasks return the lastDuration field and was causing a crash
|
||||
- **network**: Fixed an issue with dynamic typing in responses from Sonarr for history items
|
||||
- **config**: The CLI panics if the servarr you specify has no config defined
|
||||
- Imported a missing macro in the panic hook
|
||||
|
||||
### Refactor
|
||||
|
||||
- **cli**: the trigger-automatic-search commands now all have their own dedicated subcommand to keep things cleaner. Now they look like 'trigger-automatic-search episode/series/season' and their corresponding flags
|
||||
- **cli**: Added an additional delegation test to ensure manual-search commands are delegated to the manual-search command handler
|
||||
- **cli**: Moved the manual-season-search and manual-episode-search commands into their own dedicated handler so the commands can now be manual-search episode or manual-search season
|
||||
|
||||
## v0.2.2 (2024-11-06)
|
||||
|
||||
### Fix
|
||||
|
||||
- **handler**: Fixed a bug in the movie details handler that would allow key events to be processed before the data was finished loading
|
||||
- **ui**: Fixed a bug that would freeze all user input while background network requests were running
|
||||
- **radarr_ui**: Fixed a race condition bug in the movie details UI that would panic if the user changes tabs too quickly
|
||||
|
||||
### Perf
|
||||
|
||||
- **network**: Improved performance and reactiveness of the UI by speeding up network requests and clearing the channel whenever a request is cancelled/the UI is routing
|
||||
|
||||
## v0.2.1 (2024-11-06)
|
||||
|
||||
## [0.2.1](https://github.com/Dark-Alex-17/managarr/compare/v0.2.0...v0.2.1) - 2024-11-06
|
||||
|
||||
### Other
|
||||
|
||||
- Removed the need for use_ssl to indicate SSL usage; instead just use the ssl_cert_path
|
||||
- Applied bug fix to the downloads tab as well as the context [skip ci]
|
||||
- Updated the README to not include the GitHub downloads badge since all binary releases are on crates.io [skip ci]
|
||||
- Set all releases as manually triggered instead of automatic [skip ci]
|
||||
- Updated dockerfile to no longer use the --disable-terminal-size-checks flag [skip ci]
|
||||
|
||||
## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03
|
||||
|
||||
### Other
|
||||
|
||||
- Added HTTPS support for all Servarrs
|
||||
|
||||
## [0.1.4](https://github.com/Dark-Alex-17/managarr/compare/v0.1.3...v0.1.4) - 2024-11-01
|
||||
|
||||
### Other
|
||||
|
||||
- Added the ability to fetch host configs and security configs to the CLI
|
||||
- Updated README to be more clear about what features are supported [skip ci]
|
||||
|
||||
## [0.1.2](https://github.com/Dark-Alex-17/managarr/compare/v0.1.1...v0.1.2) - 2024-10-30
|
||||
|
||||
### Other
|
||||
|
||||
- Updated README to a more polished format for the alpha release
|
||||
|
||||
## [0.1.1](https://github.com/Dark-Alex-17/managarr/compare/v0.1.0...v0.1.1) - 2024-10-30
|
||||
|
||||
### Other
|
||||
|
||||
- Final dependency update
|
||||
- Updated serde version to get minimal_versions job to pass
|
||||
- Updated strum_macros dependency
|
||||
@@ -1,6 +1,7 @@
|
||||
# Contributing
|
||||
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
||||
|
||||
## Rust
|
||||
You'll need to have the stable Rust toolchain installed in order to develop Managarr.
|
||||
|
||||
The Rust toolchain (stable) can be installed via rustup using the following command:
|
||||
@@ -11,6 +12,37 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
This will install `rustup`, `rustc` and `cargo`. For more information, refer to the [official Rust installation documentation](https://www.rust-lang.org/tools/install).
|
||||
|
||||
## Commitizen
|
||||
[Commitizen](https://github.com/commitizen-tools/commitizen?tab=readme-ov-file) is a nifty tool that helps us write better commit messages. It ensures that our
|
||||
commits have a consistent style and makes it easier to generate CHANGELOGS. Additionally,
|
||||
Commitizen is used to run pre-commit checks to enforce style constraints.
|
||||
|
||||
To install `commitizen` and the `pre-commit` prerequisite, run the following command:
|
||||
|
||||
```shell
|
||||
python3 -m pip install commitizen pre-commit
|
||||
```
|
||||
|
||||
### Commitizen Quick Guide
|
||||
To see an example commit to get an idea for the Commitizen style, run:
|
||||
|
||||
```shell
|
||||
cz example
|
||||
```
|
||||
|
||||
To see the allowed types of commits and their descriptions, run:
|
||||
|
||||
```shell
|
||||
cz info
|
||||
```
|
||||
|
||||
If you'd like to create a commit using Commitizen with an interactive prompt to help you get
|
||||
comfortable with the style, use:
|
||||
|
||||
```shell
|
||||
cz commit
|
||||
```
|
||||
|
||||
## Setup workspace
|
||||
|
||||
1. Clone this repo
|
||||
|
||||
Generated
+880
-368
File diff suppressed because it is too large
Load Diff
+16
-14
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "managarr"
|
||||
version = "0.1.0"
|
||||
version = "0.3.5"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "A TUI and CLI to manage your Servarrs"
|
||||
keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"]
|
||||
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
|
||||
documentation = "https://github.com/Dark-Alex-17/managarr"
|
||||
repository = "https://github.com/Dark-Alex-17/managarr"
|
||||
homepage = "https://github.com/Dark-Alex-17/managarr"
|
||||
@@ -15,43 +15,45 @@ exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.68"
|
||||
backtrace = "0.3.67"
|
||||
backtrace = "0.3.74"
|
||||
bimap = { version = "0.6.3", features = ["serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
confy = { version = "0.6.0", default-features = false, features = [
|
||||
"yaml_conf",
|
||||
] }
|
||||
crossterm = "0.27.0"
|
||||
crossterm = "0.28.1"
|
||||
derivative = "2.2.0"
|
||||
human-panic = "1.1.3"
|
||||
human-panic = "2.0.2"
|
||||
indoc = "2.0.0"
|
||||
log = "0.4.17"
|
||||
log4rs = { version = "1.2.0", features = ["file_appender"] }
|
||||
regex = "1.7.1"
|
||||
reqwest = { version = "0.11.14", features = ["json"] }
|
||||
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", features = ["derive"] }
|
||||
strum = { version = "0.26.1", features = ["derive"] }
|
||||
strum_macros = "0.26.1"
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
strum_macros = "0.26.4"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
tokio-util = "0.7.8"
|
||||
ratatui = { version = "0.28.0", features = ["all-widgets"] }
|
||||
ratatui = { version = "0.29.0", features = ["all-widgets"] }
|
||||
urlencoding = "2.1.2"
|
||||
clap = { version = "4.5.20", features = ["derive", "cargo"] }
|
||||
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
|
||||
clap_complete = "4.5.33"
|
||||
itertools = "0.13.0"
|
||||
ctrlc = "3.4.5"
|
||||
colored = "2.1.0"
|
||||
async-trait = "0.1.83"
|
||||
dirs-next = "2.0.0"
|
||||
managarr-tree-widget = "0.24.0"
|
||||
indicatif = "0.17.9"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.16"
|
||||
mockall = "0.13.0"
|
||||
mockito = "1.0.0"
|
||||
pretty_assertions = "1.3.0"
|
||||
rstest = "0.18.2"
|
||||
rstest = "0.23.0"
|
||||
|
||||
[dev-dependencies.cargo-husky]
|
||||
version = "1"
|
||||
|
||||
@@ -32,6 +32,9 @@ lint-fix:
|
||||
fmt:
|
||||
@cargo fmt
|
||||
|
||||
minimal-versions:
|
||||
@cargo +nightly update -Zdirect-minimal-versions
|
||||
|
||||
## Analyze for unsafe usage - `cargo install cargo-geiger`
|
||||
analyze:
|
||||
@cargo geiger
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

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

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

|
||||
|
||||
Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust!
|
||||
@@ -17,14 +15,14 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
|
||||
|
||||
## What Servarrs are supported?
|
||||
|
||||
-  [Radarr](https://wiki.servarr.com/radarr)
|
||||
-  [Sonarr](https://wiki.servarr.com/en/sonarr)
|
||||
-  [Readarr](https://wiki.servarr.com/en/readarr)
|
||||
-  [Lidarr](https://wiki.servarr.com/en/lidarr)
|
||||
-  [Prowlarr](https://wiki.servarr.com/en/prowlarr)
|
||||
-  [Whisparr](https://wiki.servarr.com/whisparr)
|
||||
-  [Bazarr](https://www.bazarr.media/)
|
||||
-  [Tautulli](https://tautulli.com/)
|
||||
- [x]  [Radarr](https://wiki.servarr.com/radarr)
|
||||
- [x]  [Sonarr](https://wiki.servarr.com/en/sonarr)
|
||||
- [ ]  [Readarr](https://wiki.servarr.com/en/readarr)
|
||||
- [ ]  [Lidarr](https://wiki.servarr.com/en/lidarr)
|
||||
- [ ]  [Prowlarr](https://wiki.servarr.com/en/prowlarr)
|
||||
- [ ]  [Whisparr](https://wiki.servarr.com/whisparr)
|
||||
- [ ]  [Bazarr](https://www.bazarr.media/)
|
||||
- [ ]  [Tautulli](https://tautulli.com/)
|
||||
|
||||
## Try Before You Buy
|
||||
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
|
||||
@@ -45,28 +43,67 @@ cargo install managarr
|
||||
cargo install --locked managarr
|
||||
```
|
||||
|
||||
### Docker
|
||||
Run Managarr as a docker container by mounting your `config.yml` file to `/root/.config/managarr/config.yml`. For example:
|
||||
```shell
|
||||
docker run --rm -it -v ~/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr
|
||||
```
|
||||
|
||||
You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command.
|
||||
|
||||
Please note that you will need to create and popular your configuration file first before starting the container. Otherwise, the container will fail to start.
|
||||
|
||||
## Features
|
||||
Key:
|
||||
|
||||
| Symbol | Status |
|
||||
|--------|-----------|
|
||||
| ✅ | Supported |
|
||||
| ❌ | Missing |
|
||||
| 🕒 | Planned |
|
||||
| 🚫 | Won't Add |
|
||||
|
||||
### Radarr
|
||||
|
||||
- [x] View your library, downloads, collections, and blocklist
|
||||
- [x] View details of a specific movie including description, history, downloaded file info, or the credits
|
||||
- [x] View details of any collection and the movies in them
|
||||
- [x] Search your library or collections
|
||||
- [x] Add movies to your library
|
||||
- [x] Delete movies, downloads, and indexers
|
||||
- [x] Trigger automatic searches for movies
|
||||
- [x] Trigger refresh and disk scan for movies, downloads, and collections
|
||||
- [x] Manually search for movies
|
||||
- [x] Edit your movies, collections, and indexers
|
||||
- [x] Manage your tags
|
||||
- [x] Manage your root folders
|
||||
- [x] Manage your blocklist
|
||||
- [x] View and browse logs, tasks, events queues, and updates
|
||||
- [x] Manually trigger scheduled tasks
|
||||
| TUI | CLI | Feature |
|
||||
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||
| ✅ | ✅ | View your library, downloads, collections, and blocklist |
|
||||
| ✅ | ✅ | View details of a specific movie including description, history, downloaded file info, or the credits |
|
||||
| ✅ | ✅ | View details of any collection and the movies in them |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library or collections |
|
||||
| ✅ | ✅ | Add movies to your library |
|
||||
| ✅ | ✅ | Delete movies, downloads, and indexers |
|
||||
| ✅ | ✅ | Trigger automatic searches for movies |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
|
||||
| ✅ | ✅ | Manually search for movies |
|
||||
| ✅ | ✅ | Edit your movies, collections, 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 |
|
||||
|
||||
### Sonarr
|
||||
- [ ] Support for Sonarr
|
||||
|
||||
| TUI | CLI | Feature |
|
||||
|-----|-----|--------------------------------------------------------------------------------------------------------------------|
|
||||
| 🕒 | ✅ | View your library, downloads, blocklist, episodes |
|
||||
| 🕒 | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
|
||||
| 🕒 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| 🕒 | ✅ | Search your library |
|
||||
| 🕒 | ✅ | Add series to your library |
|
||||
| 🕒 | ✅ | Delete series, downloads, indexers, root folders, and episode files |
|
||||
| 🕒 | ✅ | Mark history events as failed |
|
||||
| 🕒 | ✅ | Trigger automatic searches for series, seasons, or episodes |
|
||||
| 🕒 | ✅ | Trigger refresh and disk scan for series and downloads |
|
||||
| 🕒 | ✅ | Manually search for series, seasons, or episodes |
|
||||
| 🕒 | ✅ | Edit your series 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
|
||||
|
||||
@@ -95,58 +132,66 @@ cargo install --locked managarr
|
||||
### The Managarr CLI
|
||||
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
|
||||
|
||||
All management features available in the TUI are also available in the CLI.
|
||||
All management features available in the TUI are also available in the CLI. However, the CLI is
|
||||
equipped with additional features to allow for more advanced usage and automation.
|
||||
|
||||
The CLI can be helpful for automating tasks or for use in scripts. For example, you can use the CLI to trigger a search for a movie, or to add a movie to your library.
|
||||
The CLI can be helpful for automating tasks or for use in scripts. For example, you can use the CLI to trigger a search for a movie, or to add a movie to your Radarr library.
|
||||
|
||||
To see all available commands, simply run `managarr --help`:
|
||||
|
||||
```shell
|
||||
$ managarr --help
|
||||
managarr 0.0.36
|
||||
managarr 0.3.0
|
||||
Alex Clarke <alex.j.tusa@gmail.com>
|
||||
|
||||
A TUI and CLI to manage your Servarrs
|
||||
|
||||
Usage: managarr [COMMAND]
|
||||
Usage: managarr [OPTIONS] [COMMAND]
|
||||
|
||||
Commands:
|
||||
radarr Commands for manging your Radarr instance
|
||||
sonarr Commands for manging your Sonarr instance
|
||||
completions Generate shell completions for the Managarr CLI
|
||||
tail-logs Tail Managarr logs
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Radarr, you would run:
|
||||
All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run:
|
||||
|
||||
```shell
|
||||
$ managarr radarr --help
|
||||
Commands for manging your Radarr instance
|
||||
$ managarr sonarr --help
|
||||
Commands for manging your Sonarr instance
|
||||
|
||||
Usage: managarr radarr <COMMAND>
|
||||
Usage: managarr sonarr [OPTIONS] <COMMAND>
|
||||
|
||||
Commands:
|
||||
add Commands to add or create new resources within your Radarr instance
|
||||
delete Commands to delete resources from your Radarr instance
|
||||
edit Commands to edit resources in your Radarr instance
|
||||
get Commands to fetch details of the resources in your Radarr instance
|
||||
list Commands to list attributes from your Radarr instance
|
||||
refresh Commands to refresh the data in your Radarr instance
|
||||
clear-blocklist Clear the blocklist
|
||||
download-release Manually download the given release for the specified movie ID
|
||||
manual-search Trigger a manual search of releases for the movie with the given ID
|
||||
search-new-movie Search for a new film to add to Radarr
|
||||
start-task Start the specified Radarr task
|
||||
test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'
|
||||
test-all-indexers Test all indexers
|
||||
trigger-automatic-search Trigger an automatic search for the movie with the specified ID
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
add Commands to add or create new resources within your Sonarr instance
|
||||
delete Commands to delete resources from your Sonarr instance
|
||||
edit Commands to edit resources in your Sonarr instance
|
||||
get Commands to fetch details of the resources in your Sonarr instance
|
||||
download Commands to download releases in your Sonarr instance
|
||||
list Commands to list attributes from your Sonarr instance
|
||||
refresh Commands to refresh the data in your Sonarr instance
|
||||
manual-search Commands to manually search for releases
|
||||
trigger-automatic-search Commands to trigger automatic searches for releases of different resources in your Sonarr instance
|
||||
clear-blocklist Clear the blocklist
|
||||
mark-history-item-as-failed Mark the Sonarr history item with the given ID as 'failed'
|
||||
search-new-series Search for a new series to add to Sonarr
|
||||
start-task Start the specified Sonarr task
|
||||
test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'
|
||||
test-all-indexers Test all Sonarr indexers
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
**Pro Tip:** The CLI is even more powerful and useful when used in conjunction with the `jq` CLI tool. This allows you to parse the JSON response from the Managarr CLI and use it in your scripts; For example, to extract the `movieId` of the movie "Ad Astra", you would run:
|
||||
@@ -156,21 +201,11 @@ $ managarr radarr list movies | jq '.[] | select(.title == "Ad Astra") | .id'
|
||||
277
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Docker
|
||||
Run Managarr as a docker container by mounting your `config.yml` file to `/root/.config/managarr/config.yml`. For example:
|
||||
```shell
|
||||
docker run --rm -it -v ~/.config/managarr:/root/.config/managarr darkalex17/managarr
|
||||
```
|
||||
|
||||
You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command.
|
||||
|
||||
# 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.
|
||||
|
||||
The configuration file is located somewhere different for each OS
|
||||
The configuration file is located somewhere different for each OS.
|
||||
|
||||
### Linux
|
||||
```
|
||||
@@ -187,42 +222,61 @@ $HOME/Library/Application Support/managarr/config.yml
|
||||
%APPDATA%/Roaming/managarr/config.yml
|
||||
```
|
||||
|
||||
## Specify Which Configuration File to Use
|
||||
It can sometimes be useful to specify the configuration file you wish to use. This is useful in cases
|
||||
where you may have more than one instance of a given Servarr running. Thus, you can specify the
|
||||
config file using the `--config` flag:
|
||||
|
||||
```shell
|
||||
managarr --config /path/to/config.yml
|
||||
```
|
||||
|
||||
### Example Configuration:
|
||||
```yaml
|
||||
radarr:
|
||||
host: 127.0.0.1
|
||||
host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: someApiToken1234567890
|
||||
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
|
||||
sonarr:
|
||||
host: 127.0.0.1
|
||||
port: 8989
|
||||
uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
|
||||
api_token: someApiToken1234567890
|
||||
readarr:
|
||||
host: 127.0.0.1
|
||||
host: 192.168.0.87
|
||||
port: 8787
|
||||
api_token: someApiToken1234567890
|
||||
lidarr:
|
||||
host: 127.0.0.1
|
||||
host: 192.168.0.86
|
||||
port: 8686
|
||||
api_token: someApiToken1234567890
|
||||
whisparr:
|
||||
host: 127.0.0.1
|
||||
host: 192.168.0.69
|
||||
port: 6969
|
||||
api_token: someApiToken1234567890
|
||||
ssl_cert_path: /path/to/whisparr.crt
|
||||
bazarr:
|
||||
host: 127.0.0.1
|
||||
host: 192.168.0.67
|
||||
port: 6767
|
||||
api_token: someApiToken1234567890
|
||||
prowlarr:
|
||||
host: 127.0.0.1
|
||||
host: 192.168.0.96
|
||||
port: 9696
|
||||
api_token: someApiToken1234567890
|
||||
tautulli:
|
||||
host: 127.0.0.1
|
||||
host: 192.168.0.81
|
||||
port: 8181
|
||||
api_token: someApiToken1234567890
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
Managarr supports using environment variables on startup so you don't have to always specify certain flags:
|
||||
|
||||
| Variable | Description | Equivalent Flag |
|
||||
|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
|
||||
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
|
||||
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
|
||||
|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
|
||||
|
||||
## Track My Progress for the Beta release (With Sonarr Support!)
|
||||
Progress for the beta release can be followed on my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr)
|
||||
with all items tagged `Beta`.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
coverage:
|
||||
range: "80..100"
|
||||
|
||||
ignore:
|
||||
- "**/*_tests.rs"
|
||||
- "src/ui"
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "managarr",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
+62
-15
@@ -1,13 +1,14 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::anyhow;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
|
||||
use crate::app::{App, Data, RadarrConfig, DEFAULT_ROUTE};
|
||||
use crate::app::{App, AppConfig, Data, ServarrConfig, DEFAULT_ROUTE};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::{HorizontallyScrollableText, Route, TabRoute};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::NetworkEvent;
|
||||
|
||||
@@ -34,7 +35,7 @@ mod tests {
|
||||
},
|
||||
TabRoute {
|
||||
title: "Sonarr",
|
||||
route: Route::Sonarr,
|
||||
route: ActiveSonarrBlock::Series.into(),
|
||||
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
|
||||
contextual_help: None,
|
||||
},
|
||||
@@ -47,6 +48,7 @@ mod tests {
|
||||
assert!(!app.is_routing);
|
||||
assert!(!app.should_refresh);
|
||||
assert!(!app.should_ignore_quit_key);
|
||||
assert!(!app.cli_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -87,7 +89,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_reset_cancellation_token() {
|
||||
let mut app = App::default();
|
||||
let mut app = App {
|
||||
is_loading: true,
|
||||
should_refresh: false,
|
||||
..App::default()
|
||||
};
|
||||
app.cancellation_token.cancel();
|
||||
|
||||
assert!(app.cancellation_token.is_cancelled());
|
||||
@@ -96,6 +102,8 @@ mod tests {
|
||||
|
||||
assert!(!app.cancellation_token.is_cancelled());
|
||||
assert!(!new_token.is_cancelled());
|
||||
assert!(!app.is_loading);
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -120,6 +128,10 @@ mod tests {
|
||||
version: "test".to_owned(),
|
||||
..RadarrData::default()
|
||||
},
|
||||
sonarr_data: SonarrData {
|
||||
version: "test".to_owned(),
|
||||
..SonarrData::default()
|
||||
},
|
||||
},
|
||||
..App::default()
|
||||
};
|
||||
@@ -129,6 +141,7 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert_eq!(app.error, HorizontallyScrollableText::default());
|
||||
assert!(app.data.radarr_data.version.is_empty());
|
||||
assert!(app.data.sonarr_data.version.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -145,6 +158,29 @@ mod tests {
|
||||
assert_eq!(app.error.text, test_string);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_network_event() {
|
||||
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
|
||||
let mut app = App {
|
||||
tick_until_poll: 2,
|
||||
network_tx: Some(sync_network_tx),
|
||||
..App::default()
|
||||
};
|
||||
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
app
|
||||
.dispatch_network_event(RadarrEvent::GetStatus.into())
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetStatus.into()
|
||||
);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_tick_first_render() {
|
||||
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
@@ -158,6 +194,7 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
app.on_tick(true).await;
|
||||
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetQualityProfiles.into()
|
||||
@@ -172,7 +209,11 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetOverview.into()
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDiskSpace.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
@@ -182,10 +223,6 @@ mod tests {
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetMovies.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(!app.is_routing);
|
||||
assert!(!app.should_refresh);
|
||||
assert_eq!(app.tick_count, 1);
|
||||
@@ -218,11 +255,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radarr_config_default() {
|
||||
let radarr_config = RadarrConfig::default();
|
||||
fn test_app_config_default() {
|
||||
let app_config = AppConfig::default();
|
||||
|
||||
assert_str_eq!(radarr_config.host, "localhost");
|
||||
assert_eq!(radarr_config.port, Some(7878));
|
||||
assert!(radarr_config.api_token.is_empty());
|
||||
assert!(app_config.radarr.is_none());
|
||||
assert!(app_config.sonarr.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_servarr_config_default() {
|
||||
let servarr_config = ServarrConfig::default();
|
||||
|
||||
assert_eq!(servarr_config.host, Some("localhost".to_string()));
|
||||
assert_eq!(servarr_config.port, None);
|
||||
assert_eq!(servarr_config.uri, None);
|
||||
assert!(servarr_config.api_token.is_empty());
|
||||
assert_eq!(servarr_config.ssl_cert_path, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ generate_keybindings! {
|
||||
tab,
|
||||
delete,
|
||||
submit,
|
||||
confirm,
|
||||
quit,
|
||||
esc
|
||||
}
|
||||
@@ -140,6 +141,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
|
||||
key: Key::Enter,
|
||||
desc: "submit",
|
||||
},
|
||||
confirm: KeyBinding {
|
||||
key: Key::Ctrl('s'),
|
||||
desc: "submit",
|
||||
},
|
||||
quit: KeyBinding {
|
||||
key: Key::Char('q'),
|
||||
desc: "quit",
|
||||
|
||||
@@ -31,6 +31,7 @@ mod test {
|
||||
#[case(DEFAULT_KEYBINDINGS.tab, Key::Tab, "tab")]
|
||||
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")]
|
||||
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")]
|
||||
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")]
|
||||
#[case(DEFAULT_KEYBINDINGS.quit, Key::Char('q'), "quit")]
|
||||
#[case(DEFAULT_KEYBINDINGS.esc, Key::Esc, "close")]
|
||||
fn test_default_key_bindings_and_descriptions(
|
||||
|
||||
+80
-16
@@ -1,11 +1,16 @@
|
||||
use std::process;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use colored::Colorize;
|
||||
use log::{debug, error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
|
||||
use crate::cli::Command;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
|
||||
use crate::network::NetworkEvent;
|
||||
|
||||
@@ -32,6 +37,7 @@ pub struct App<'a> {
|
||||
pub is_loading: bool,
|
||||
pub should_refresh: bool,
|
||||
pub should_ignore_quit_key: bool,
|
||||
pub cli_mode: bool,
|
||||
pub config: AppConfig,
|
||||
pub data: Data<'a>,
|
||||
}
|
||||
@@ -53,7 +59,10 @@ impl<'a> App<'a> {
|
||||
pub async fn dispatch_network_event(&mut self, action: NetworkEvent) {
|
||||
debug!("Dispatching network event: {action:?}");
|
||||
|
||||
self.is_loading = true;
|
||||
if !self.should_refresh {
|
||||
self.is_loading = true;
|
||||
}
|
||||
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
if let Err(e) = network_tx.send(action).await {
|
||||
self.is_loading = false;
|
||||
@@ -110,6 +119,8 @@ impl<'a> App<'a> {
|
||||
|
||||
pub fn reset_cancellation_token(&mut self) -> CancellationToken {
|
||||
self.cancellation_token = CancellationToken::new();
|
||||
self.should_refresh = true;
|
||||
self.is_loading = false;
|
||||
|
||||
self.cancellation_token.clone()
|
||||
}
|
||||
@@ -143,7 +154,7 @@ impl<'a> Default for App<'a> {
|
||||
},
|
||||
TabRoute {
|
||||
title: "Sonarr",
|
||||
route: Route::Sonarr,
|
||||
route: ActiveSonarrBlock::Series.into(),
|
||||
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
|
||||
contextual_help: None,
|
||||
},
|
||||
@@ -155,6 +166,7 @@ impl<'a> Default for App<'a> {
|
||||
is_routing: false,
|
||||
should_refresh: false,
|
||||
should_ignore_quit_key: false,
|
||||
cli_mode: false,
|
||||
config: AppConfig::default(),
|
||||
data: Data::default(),
|
||||
}
|
||||
@@ -164,26 +176,78 @@ impl<'a> Default for App<'a> {
|
||||
#[derive(Default)]
|
||||
pub struct Data<'a> {
|
||||
pub radarr_data: RadarrData<'a>,
|
||||
pub sonarr_data: SonarrData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Default)]
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub radarr: RadarrConfig,
|
||||
pub radarr: Option<ServarrConfig>,
|
||||
pub sonarr: Option<ServarrConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct RadarrConfig {
|
||||
pub host: String,
|
||||
pub port: Option<u16>,
|
||||
pub api_token: String,
|
||||
}
|
||||
impl AppConfig {
|
||||
pub fn validate(&self) {
|
||||
if let Some(radarr_config) = &self.radarr {
|
||||
radarr_config.validate();
|
||||
}
|
||||
|
||||
impl Default for RadarrConfig {
|
||||
fn default() -> Self {
|
||||
RadarrConfig {
|
||||
host: "localhost".to_string(),
|
||||
port: Some(7878),
|
||||
api_token: "".to_string(),
|
||||
if let Some(sonarr_config) = &self.sonarr {
|
||||
sonarr_config.validate();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_config_present_for_cli(&self, command: &Command) {
|
||||
let msg = |servarr: &str| {
|
||||
log_and_print_error(format!(
|
||||
"{} configuration missing; Unable to run any {} commands.",
|
||||
servarr, servarr
|
||||
))
|
||||
};
|
||||
match command {
|
||||
Command::Radarr(_) if self.radarr.is_none() => {
|
||||
msg("Radarr");
|
||||
process::exit(1);
|
||||
}
|
||||
Command::Sonarr(_) if self.sonarr.is_none() => {
|
||||
msg("Sonarr");
|
||||
process::exit(1);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ServarrConfig {
|
||||
pub host: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub uri: Option<String>,
|
||||
pub api_token: String,
|
||||
pub ssl_cert_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ServarrConfig {
|
||||
fn validate(&self) {
|
||||
if self.host.is_none() && self.uri.is_none() {
|
||||
log_and_print_error("'host' or 'uri' is required for configuration".to_owned());
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServarrConfig {
|
||||
fn default() -> Self {
|
||||
ServarrConfig {
|
||||
host: Some("localhost".to_string()),
|
||||
port: None,
|
||||
uri: None,
|
||||
api_token: "".to_string(),
|
||||
ssl_cert_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_and_print_error(error: String) {
|
||||
error!("{}", error);
|
||||
eprintln!("error: {}", error.red());
|
||||
}
|
||||
|
||||
+12
-19
@@ -142,35 +142,22 @@ impl<'a> App<'a> {
|
||||
is_first_render: bool,
|
||||
) {
|
||||
if is_first_render {
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetOverview.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetStatus.into())
|
||||
.await;
|
||||
self.refresh_metadata().await;
|
||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||
}
|
||||
|
||||
if self.should_refresh {
|
||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||
self.refresh_metadata().await;
|
||||
}
|
||||
|
||||
if self.is_routing {
|
||||
if self.is_loading && !self.should_refresh {
|
||||
if !self.should_refresh {
|
||||
self.cancellation_token.cancel();
|
||||
} else {
|
||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||
self.refresh_metadata().await;
|
||||
}
|
||||
|
||||
self.dispatch_by_radarr_block(&active_radarr_block).await;
|
||||
self.refresh_metadata().await;
|
||||
}
|
||||
|
||||
if self.tick_count % self.tick_until_poll == 0 {
|
||||
@@ -191,6 +178,12 @@ impl<'a> App<'a> {
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetDownloads.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetDiskSpace.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetStatus.into())
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn populate_movie_collection_table(&mut self) {
|
||||
|
||||
@@ -120,6 +120,11 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.confirm, "submit"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel"),
|
||||
];
|
||||
|
||||
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "start task"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
|
||||
@@ -5,8 +5,8 @@ mod tests {
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::radarr::radarr_context_clues::{
|
||||
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
|
||||
COLLECTION_DETAILS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
|
||||
COLLECTION_DETAILS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
|
||||
INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
|
||||
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
|
||||
SYSTEM_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
@@ -349,6 +349,22 @@ mod tests {
|
||||
assert_eq!(add_movie_search_results_context_clues_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_confirmation_prompt_context_clues() {
|
||||
let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter();
|
||||
|
||||
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
|
||||
|
||||
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm);
|
||||
assert_str_eq!(*description, "submit");
|
||||
|
||||
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
|
||||
|
||||
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
|
||||
assert_str_eq!(*description, "cancel");
|
||||
assert_eq!(confirmation_prompt_context_clues_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_tasks_context_clues() {
|
||||
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
|
||||
|
||||
@@ -6,7 +6,7 @@ mod tests {
|
||||
|
||||
use crate::app::radarr::ActiveRadarrBlock;
|
||||
use crate::app::App;
|
||||
use crate::models::radarr_models::{Collection, CollectionMovie, Credit, Release};
|
||||
use crate::models::radarr_models::{Collection, CollectionMovie, Credit, RadarrRelease};
|
||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
let mut movie_details_modal = MovieDetailsModal::default();
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(vec![Release::default()]);
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
|
||||
app
|
||||
@@ -508,6 +508,14 @@ mod tests {
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDiskSpace.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetStatus.into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
}
|
||||
|
||||
@@ -531,16 +539,16 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetOverview.into()
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDiskSpace.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetStatus.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
}
|
||||
@@ -549,6 +557,7 @@ mod tests {
|
||||
async fn test_radarr_on_tick_routing() {
|
||||
let (mut app, mut sync_network_rx) = construct_app_unit();
|
||||
app.is_routing = true;
|
||||
app.should_refresh = true;
|
||||
|
||||
app
|
||||
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
|
||||
@@ -574,43 +583,19 @@ mod tests {
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
|
||||
let (mut app, mut sync_network_rx) = construct_app_unit();
|
||||
let (mut app, _) = construct_app_unit();
|
||||
app.is_routing = true;
|
||||
app.is_loading = true;
|
||||
app.should_refresh = false;
|
||||
|
||||
app
|
||||
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetTags.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
assert!(app.cancellation_token.is_cancelled());
|
||||
}
|
||||
|
||||
@@ -627,7 +612,6 @@ mod tests {
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetDownloads.into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(app.should_refresh);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
+57
-10
@@ -10,19 +10,28 @@ mod tests {
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand},
|
||||
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand},
|
||||
models::{
|
||||
radarr_models::{BlocklistItem, BlocklistResponse, RadarrSerdeable},
|
||||
radarr_models::{
|
||||
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
|
||||
RadarrSerdeable,
|
||||
},
|
||||
sonarr_models::{
|
||||
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
|
||||
SonarrSerdeable,
|
||||
},
|
||||
Serdeable,
|
||||
},
|
||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
network::{
|
||||
radarr_network::RadarrEvent, sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent,
|
||||
},
|
||||
Cli,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr"]);
|
||||
#[rstest]
|
||||
fn test_servarr_subcommand_requires_subcommand(#[values("radarr", "sonarr")] subcommand: &str) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", subcommand]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
@@ -39,6 +48,13 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_subcommand_delegates_to_sonarr() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completions_requires_argument() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||
@@ -106,8 +122,8 @@ mod tests {
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse(
|
||||
BlocklistResponse {
|
||||
records: vec![BlocklistItem::default()],
|
||||
RadarrBlocklistResponse {
|
||||
records: vec![RadarrBlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
@@ -121,9 +137,40 @@ mod tests {
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let claer_blocklist_command = RadarrCommand::ClearBlocklist.into();
|
||||
let clear_blocklist_command = RadarrCommand::ClearBlocklist.into();
|
||||
|
||||
let result = handle_command(&app_arc, claer_blocklist_command, &mut mock_network).await;
|
||||
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cli_handler_delegates_sonarr_commands_to_the_sonarr_cli_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::BlocklistResponse(
|
||||
SonarrBlocklistResponse {
|
||||
records: vec![SonarrBlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let clear_blocklist_command = SonarrCommand::ClearBlocklist.into();
|
||||
|
||||
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
+28
-21
@@ -4,11 +4,13 @@ use anyhow::Result;
|
||||
use clap::{command, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cli_tests.rs"]
|
||||
@@ -19,6 +21,9 @@ pub enum Command {
|
||||
#[command(subcommand, about = "Commands for manging your Radarr instance")]
|
||||
Radarr(RadarrCommand),
|
||||
|
||||
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
|
||||
Sonarr(SonarrCommand),
|
||||
|
||||
#[command(
|
||||
arg_required_else_help = true,
|
||||
about = "Generate shell completions for the Managarr CLI"
|
||||
@@ -27,24 +32,39 @@ pub enum Command {
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
},
|
||||
|
||||
#[command(about = "Tail Managarr logs")]
|
||||
TailLogs {
|
||||
#[arg(long, help = "Disable colored log output")]
|
||||
no_color: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
|
||||
fn with(app: &'a Arc<Mutex<App<'b>>>, command: T, network: &'a mut dyn NetworkTrait) -> Self;
|
||||
async fn handle(self) -> Result<()>;
|
||||
async fn handle(self) -> Result<String>;
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_command(
|
||||
app: &Arc<Mutex<App<'_>>>,
|
||||
command: Command,
|
||||
network: &mut dyn NetworkTrait,
|
||||
) -> Result<()> {
|
||||
if let Command::Radarr(radarr_command) = command {
|
||||
RadarrCliHandler::with(app, radarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Ok(())
|
||||
) -> Result<String> {
|
||||
let result = match command {
|
||||
Command::Radarr(radarr_command) => {
|
||||
RadarrCliHandler::with(app, radarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Command::Sonarr(sonarr_command) => {
|
||||
SonarrCliHandler::with(app, sonarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -68,16 +88,3 @@ pub fn mutex_flags_or_default(positive: bool, negative: bool, default_value: boo
|
||||
default_value
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! execute_network_event {
|
||||
($self:ident, $event:expr) => {
|
||||
let resp = $self.network.handle_network_event($event.into()).await?;
|
||||
let json = serde_json::to_string_pretty(&resp)?;
|
||||
println!("{}", json);
|
||||
};
|
||||
($self:ident, $event:expr, $happy_output:expr) => {
|
||||
$self.network.handle_network_event($event.into()).await?;
|
||||
println!("{}", $happy_output);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
models::radarr_models::{AddMovieBody, AddOptions, MinimumAvailability, Monitor},
|
||||
models::radarr_models::{AddMovieBody, AddMovieOptions, MinimumAvailability, MovieMonitor},
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
@@ -47,7 +46,7 @@ pub enum RadarrAddCommand {
|
||||
default_value_t = MinimumAvailability::default()
|
||||
)]
|
||||
minimum_availability: MinimumAvailability,
|
||||
#[arg(long, help = "Should Radarr monitor this film")]
|
||||
#[arg(long, help = "Disable monitoring for this film")]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
@@ -60,9 +59,9 @@ pub enum RadarrAddCommand {
|
||||
long,
|
||||
help = "What Radarr should monitor",
|
||||
value_enum,
|
||||
default_value_t = Monitor::default()
|
||||
default_value_t = MovieMonitor::default()
|
||||
)]
|
||||
monitor: Monitor,
|
||||
monitor: MovieMonitor,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tell Radarr to not start a search for this film once it's added to your library"
|
||||
@@ -106,8 +105,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrAddCommand::Movie {
|
||||
tmdb_id,
|
||||
root_folder_path,
|
||||
@@ -126,24 +125,33 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
|
||||
minimum_availability: minimum_availability.to_string(),
|
||||
monitored: !disable_monitoring,
|
||||
tags,
|
||||
add_options: AddOptions {
|
||||
add_options: AddMovieOptions {
|
||||
monitor: monitor.to_string(),
|
||||
search_for_movie: !no_search_for_movie,
|
||||
},
|
||||
};
|
||||
execute_network_event!(self, RadarrEvent::AddMovie(Some(body)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::AddMovie(Some(body)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrAddCommand::RootFolder { root_folder_path } => {
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::AddRootFolder(Some(root_folder_path.clone()))
|
||||
);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::AddRootFolder(Some(root_folder_path)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrAddCommand::Tag { name } => {
|
||||
execute_network_event!(self, RadarrEvent::AddTag(name.clone()));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::AddTag(name).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ mod tests {
|
||||
radarr::{add_command_handler::RadarrAddCommand, RadarrCommand},
|
||||
Command,
|
||||
},
|
||||
models::radarr_models::{MinimumAvailability, Monitor},
|
||||
models::radarr_models::{MinimumAvailability, MovieMonitor},
|
||||
Cli,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_add_command_from() {
|
||||
@@ -111,6 +112,8 @@ mod tests {
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--tmdb-id",
|
||||
"1",
|
||||
flag,
|
||||
]);
|
||||
|
||||
@@ -187,7 +190,7 @@ mod tests {
|
||||
minimum_availability: MinimumAvailability::default(),
|
||||
disable_monitoring: false,
|
||||
tag: vec![],
|
||||
monitor: Monitor::default(),
|
||||
monitor: MovieMonitor::default(),
|
||||
no_search_for_movie: false,
|
||||
};
|
||||
|
||||
@@ -219,7 +222,7 @@ mod tests {
|
||||
minimum_availability: MinimumAvailability::default(),
|
||||
disable_monitoring: false,
|
||||
tag: vec![1, 2],
|
||||
monitor: Monitor::default(),
|
||||
monitor: MovieMonitor::default(),
|
||||
no_search_for_movie: false,
|
||||
};
|
||||
|
||||
@@ -255,7 +258,7 @@ mod tests {
|
||||
minimum_availability: MinimumAvailability::Released,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: Monitor::MovieAndCollection,
|
||||
monitor: MovieMonitor::MovieAndCollection,
|
||||
no_search_for_movie: true,
|
||||
};
|
||||
|
||||
@@ -356,7 +359,7 @@ mod tests {
|
||||
app::App,
|
||||
cli::{radarr::add_command_handler::RadarrAddCommandHandler, CliCommandHandler},
|
||||
models::{
|
||||
radarr_models::{AddMovieBody, AddOptions, RadarrSerdeable},
|
||||
radarr_models::{AddMovieBody, AddMovieOptions, RadarrSerdeable},
|
||||
Serdeable,
|
||||
},
|
||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
@@ -378,7 +381,7 @@ mod tests {
|
||||
minimum_availability: "released".to_owned(),
|
||||
monitored: false,
|
||||
tags: vec![1, 2],
|
||||
add_options: AddOptions {
|
||||
add_options: AddMovieOptions {
|
||||
monitor: "movieAndCollection".to_owned(),
|
||||
search_for_movie: false,
|
||||
},
|
||||
@@ -403,7 +406,7 @@ mod tests {
|
||||
minimum_availability: MinimumAvailability::Released,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: Monitor::MovieAndCollection,
|
||||
monitor: MovieMonitor::MovieAndCollection,
|
||||
no_search_for_movie: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
models::radarr_models::DeleteMovieParams,
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
@@ -85,19 +84,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))
|
||||
);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrDeleteCommand::Download { download_id } => {
|
||||
execute_network_event!(self, RadarrEvent::DeleteDownload(Some(download_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::DeleteDownload(Some(download_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrDeleteCommand::Indexer { indexer_id } => {
|
||||
execute_network_event!(self, RadarrEvent::DeleteIndexer(Some(indexer_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::DeleteIndexer(Some(indexer_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrDeleteCommand::Movie {
|
||||
movie_id,
|
||||
@@ -109,16 +117,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
|
||||
delete_movie_files: delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
};
|
||||
execute_network_event!(self, RadarrEvent::DeleteMovie(Some(delete_movie_params)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::DeleteMovie(Some(delete_movie_params)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrDeleteCommand::RootFolder { root_folder_id } => {
|
||||
execute_network_event!(self, RadarrEvent::DeleteRootFolder(Some(root_folder_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::DeleteRootFolder(Some(root_folder_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrDeleteCommand::Tag { tag_id } => {
|
||||
execute_network_event!(self, RadarrEvent::DeleteTag(tag_id));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::DeleteTag(tag_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ mod tests {
|
||||
Cli,
|
||||
};
|
||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_delete_command_from() {
|
||||
|
||||
@@ -7,12 +7,11 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
models::{
|
||||
radarr_models::{
|
||||
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
|
||||
MinimumAvailability, RadarrSerdeable,
|
||||
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability, RadarrSerdeable,
|
||||
},
|
||||
servarr_models::EditIndexerParams,
|
||||
Serdeable,
|
||||
},
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
@@ -339,8 +338,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrEditCommand::AllIndexerSettings {
|
||||
allow_hardcoded_subs,
|
||||
disable_allow_hardcoded_subs,
|
||||
@@ -389,11 +388,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
})
|
||||
.into(),
|
||||
};
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::EditAllIndexerSettings(Some(params)),
|
||||
"All indexer settings updated"
|
||||
);
|
||||
self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::EditAllIndexerSettings(Some(params)).into())
|
||||
.await?;
|
||||
"All indexer settings updated".to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
RadarrEditCommand::Collection {
|
||||
@@ -417,11 +418,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
root_folder_path,
|
||||
search_on_add: search_on_add_value,
|
||||
};
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::EditCollection(Some(edit_collection_params)),
|
||||
"Collection Updated"
|
||||
);
|
||||
self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::EditCollection(Some(edit_collection_params)).into())
|
||||
.await?;
|
||||
"Collection updated".to_owned()
|
||||
}
|
||||
RadarrEditCommand::Indexer {
|
||||
indexer_id,
|
||||
@@ -458,11 +459,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::EditIndexer(Some(edit_indexer_params)),
|
||||
"Indexer updated"
|
||||
);
|
||||
self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)).into())
|
||||
.await?;
|
||||
"Indexer updated".to_owned()
|
||||
}
|
||||
RadarrEditCommand::Movie {
|
||||
movie_id,
|
||||
@@ -485,14 +486,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
execute_network_event!(
|
||||
self,
|
||||
RadarrEvent::EditMovie(Some(edit_movie_params)),
|
||||
"Movie updated"
|
||||
);
|
||||
self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::EditMovie(Some(edit_movie_params)).into())
|
||||
.await?;
|
||||
"Movie Updated".to_owned()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ mod tests {
|
||||
Cli,
|
||||
};
|
||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_edit_command_from() {
|
||||
@@ -809,9 +810,10 @@ mod tests {
|
||||
},
|
||||
models::{
|
||||
radarr_models::{
|
||||
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
|
||||
MinimumAvailability, RadarrSerdeable,
|
||||
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability,
|
||||
RadarrSerdeable,
|
||||
},
|
||||
servarr_models::EditIndexerParams,
|
||||
Serdeable,
|
||||
},
|
||||
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
|
||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
@@ -21,6 +20,8 @@ mod get_command_handler_tests;
|
||||
pub enum RadarrGetCommand {
|
||||
#[command(about = "Get the shared settings for all indexers")]
|
||||
AllIndexerSettings,
|
||||
#[command(about = "Fetch the host config for your Radarr instance")]
|
||||
HostConfig,
|
||||
#[command(about = "Get detailed information for the movie with the given ID")]
|
||||
MovieDetails {
|
||||
#[arg(
|
||||
@@ -39,6 +40,8 @@ pub enum RadarrGetCommand {
|
||||
)]
|
||||
movie_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch the security config for your Radarr instance")]
|
||||
SecurityConfig,
|
||||
#[command(about = "Get the system status")]
|
||||
SystemStatus,
|
||||
}
|
||||
@@ -68,22 +71,52 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrGetCommand::AllIndexerSettings => {
|
||||
execute_network_event!(self, RadarrEvent::GetAllIndexerSettings);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetAllIndexerSettings.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrGetCommand::HostConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetHostConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrGetCommand::MovieDetails { movie_id } => {
|
||||
execute_network_event!(self, RadarrEvent::GetMovieDetails(Some(movie_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetMovieDetails(Some(movie_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrGetCommand::MovieHistory { movie_id } => {
|
||||
execute_network_event!(self, RadarrEvent::GetMovieHistory(Some(movie_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetMovieHistory(Some(movie_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrGetCommand::SecurityConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetSecurityConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrGetCommand::SystemStatus => {
|
||||
execute_network_event!(self, RadarrEvent::GetStatus);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetStatus.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use clap::error::ErrorKind;
|
||||
use clap::CommandFactory;
|
||||
|
||||
@@ -7,6 +7,7 @@ mod test {
|
||||
use crate::cli::radarr::RadarrCommand;
|
||||
use crate::cli::Command;
|
||||
use crate::Cli;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_get_command_from() {
|
||||
@@ -29,6 +30,14 @@ mod test {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_host_config_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "host-config"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_details_requires_movie_id() {
|
||||
let result =
|
||||
@@ -81,6 +90,14 @@ mod test {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_security_config_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "security-config"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_status_has_no_arg_requirements() {
|
||||
let result =
|
||||
@@ -135,6 +152,29 @@ mod test {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_host_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(RadarrEvent::GetHostConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_host_config_command = RadarrGetCommand::HostConfig;
|
||||
|
||||
let result =
|
||||
RadarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_movie_details_command() {
|
||||
let expected_movie_id = 1;
|
||||
@@ -187,6 +227,29 @@ mod test {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_security_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(RadarrEvent::GetSecurityConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_security_config_command = RadarrGetCommand::SecurityConfig;
|
||||
|
||||
let result =
|
||||
RadarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_system_status_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
|
||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
@@ -25,6 +24,8 @@ pub enum RadarrListCommand {
|
||||
Collections,
|
||||
#[command(about = "List all active downloads in Radarr")]
|
||||
Downloads,
|
||||
#[command(about = "List disk space details for all provisioned root folders in Radarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "List all Radarr indexers")]
|
||||
Indexers,
|
||||
#[command(about = "Fetch Radarr logs")]
|
||||
@@ -56,7 +57,7 @@ pub enum RadarrListCommand {
|
||||
RootFolders,
|
||||
#[command(about = "List all Radarr tags")]
|
||||
Tags,
|
||||
#[command(about = "List tasks")]
|
||||
#[command(about = "List all Radarr tasks")]
|
||||
Tasks,
|
||||
#[command(about = "List all Radarr updates")]
|
||||
Updates,
|
||||
@@ -87,19 +88,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrListCommand::Blocklist => {
|
||||
execute_network_event!(self, RadarrEvent::GetBlocklist);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::Collections => {
|
||||
execute_network_event!(self, RadarrEvent::GetCollections);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetCollections.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::Downloads => {
|
||||
execute_network_event!(self, RadarrEvent::GetDownloads);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetDownloads.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::DiskSpace => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetDiskSpace.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::Indexers => {
|
||||
execute_network_event!(self, RadarrEvent::GetIndexers);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::Logs {
|
||||
events,
|
||||
@@ -113,39 +137,69 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
|
||||
if output_in_log_format {
|
||||
let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone();
|
||||
|
||||
let json = serde_json::to_string_pretty(&log_lines)?;
|
||||
println!("{}", json);
|
||||
serde_json::to_string_pretty(&log_lines)?
|
||||
} else {
|
||||
let json = serde_json::to_string_pretty(&logs)?;
|
||||
println!("{}", json);
|
||||
serde_json::to_string_pretty(&logs)?
|
||||
}
|
||||
}
|
||||
RadarrListCommand::Movies => {
|
||||
execute_network_event!(self, RadarrEvent::GetMovies);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetMovies.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::MovieCredits { movie_id } => {
|
||||
execute_network_event!(self, RadarrEvent::GetMovieCredits(Some(movie_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetMovieCredits(Some(movie_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::QualityProfiles => {
|
||||
execute_network_event!(self, RadarrEvent::GetQualityProfiles);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetQualityProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::QueuedEvents => {
|
||||
execute_network_event!(self, RadarrEvent::GetQueuedEvents);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetQueuedEvents.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::RootFolders => {
|
||||
execute_network_event!(self, RadarrEvent::GetRootFolders);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetRootFolders.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::Tags => {
|
||||
execute_network_event!(self, RadarrEvent::GetTags);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetTags.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::Tasks => {
|
||||
execute_network_event!(self, RadarrEvent::GetTasks);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetTasks.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::Updates => {
|
||||
execute_network_event!(self, RadarrEvent::GetUpdates);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetUpdates.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ mod tests {
|
||||
use crate::cli::radarr::RadarrCommand;
|
||||
use crate::cli::Command;
|
||||
use crate::Cli;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_list_command_from() {
|
||||
@@ -29,6 +30,7 @@ mod tests {
|
||||
"blocklist",
|
||||
"collections",
|
||||
"downloads",
|
||||
"disk-space",
|
||||
"indexers",
|
||||
"movies",
|
||||
"quality-profiles",
|
||||
@@ -80,8 +82,8 @@ mod tests {
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command {
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
if let Some(Command::Radarr(RadarrCommand::List(credits_command))) = result.unwrap().command {
|
||||
assert_eq!(credits_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +123,7 @@ mod tests {
|
||||
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
|
||||
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
|
||||
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
|
||||
#[case(RadarrListCommand::DiskSpace, RadarrEvent::GetDiskSpace)]
|
||||
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
|
||||
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
|
||||
#[case(RadarrListCommand::QualityProfiles, RadarrEvent::GetQualityProfiles)]
|
||||
@@ -130,7 +133,7 @@ mod tests {
|
||||
#[case(RadarrListCommand::Tasks, RadarrEvent::GetTasks)]
|
||||
#[case(RadarrListCommand::Updates, RadarrEvent::GetUpdates)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_blocklist_command(
|
||||
async fn test_handle_list_command(
|
||||
#[case] list_command: RadarrListCommand,
|
||||
#[case] expected_radarr_event: RadarrEvent,
|
||||
) {
|
||||
|
||||
+50
-18
@@ -12,8 +12,7 @@ use tokio::sync::Mutex;
|
||||
use crate::app::App;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::execute_network_event;
|
||||
use crate::models::radarr_models::{ReleaseDownloadBody, TaskName};
|
||||
use crate::models::radarr_models::{RadarrReleaseDownloadBody, RadarrTaskName};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::NetworkTrait;
|
||||
use anyhow::Result;
|
||||
@@ -86,7 +85,7 @@ pub enum RadarrCommand {
|
||||
ManualSearch {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Radarr ID of the movie whose releases you wish to fetch and list",
|
||||
help = "The Radarr ID of the movie whose releases you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
movie_id: i64,
|
||||
@@ -108,7 +107,7 @@ pub enum RadarrCommand {
|
||||
value_enum,
|
||||
required = true
|
||||
)]
|
||||
task_name: TaskName,
|
||||
task_name: RadarrTaskName,
|
||||
},
|
||||
#[command(
|
||||
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
|
||||
@@ -117,7 +116,7 @@ pub enum RadarrCommand {
|
||||
#[arg(long, help = "The ID of the indexer to test", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Test all indexers")]
|
||||
#[command(about = "Test all Radarr indexers")]
|
||||
TestAllIndexers,
|
||||
#[command(about = "Trigger an automatic search for the movie with the specified ID")]
|
||||
TriggerAutomaticSearch {
|
||||
@@ -155,8 +154,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrCommand::Add(add_command) => {
|
||||
RadarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||
.handle()
|
||||
@@ -192,41 +191,74 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
execute_network_event!(self, RadarrEvent::ClearBlocklist);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::ClearBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrCommand::DownloadRelease {
|
||||
guid,
|
||||
indexer_id,
|
||||
movie_id,
|
||||
} => {
|
||||
let params = ReleaseDownloadBody {
|
||||
let params = RadarrReleaseDownloadBody {
|
||||
guid,
|
||||
indexer_id,
|
||||
movie_id,
|
||||
};
|
||||
execute_network_event!(self, RadarrEvent::DownloadRelease(Some(params)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::DownloadRelease(Some(params)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrCommand::ManualSearch { movie_id } => {
|
||||
println!("Searching for releases. This may take a minute...");
|
||||
execute_network_event!(self, RadarrEvent::GetReleases(Some(movie_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetReleases(Some(movie_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrCommand::SearchNewMovie { query } => {
|
||||
execute_network_event!(self, RadarrEvent::SearchNewMovie(Some(query)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::SearchNewMovie(Some(query)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrCommand::StartTask { task_name } => {
|
||||
execute_network_event!(self, RadarrEvent::StartTask(Some(task_name)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::StartTask(Some(task_name)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrCommand::TestIndexer { indexer_id } => {
|
||||
execute_network_event!(self, RadarrEvent::TestIndexer(Some(indexer_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::TestIndexer(Some(indexer_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrCommand::TestAllIndexers => {
|
||||
execute_network_event!(self, RadarrEvent::TestAllIndexers);
|
||||
println!("Testing all Radarr indexers. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::TestAllIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrCommand::TriggerAutomaticSearch { movie_id } => {
|
||||
execute_network_event!(self, RadarrEvent::TriggerAutomaticSearch(Some(movie_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::TriggerAutomaticSearch(Some(movie_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_download_release_requires_movie_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
@@ -50,7 +50,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_download_release_requires_guid() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
@@ -69,7 +69,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_download_release_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
@@ -105,7 +105,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_manual_search_requires_movie_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "manual-search"]);
|
||||
|
||||
@@ -129,7 +129,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_search_new_movie_requires_query() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "search-new-movie"]);
|
||||
|
||||
@@ -153,7 +153,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_start_task_requires_task_name() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "start-task"]);
|
||||
|
||||
@@ -164,7 +164,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_start_task_task_name_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
@@ -191,7 +191,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_test_indexer_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "test-indexer"]);
|
||||
|
||||
@@ -215,7 +215,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test]
|
||||
fn test_trigger_automatic_search_requires_movie_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "radarr", "trigger-automatic-search"]);
|
||||
@@ -261,8 +261,8 @@ mod tests {
|
||||
},
|
||||
models::{
|
||||
radarr_models::{
|
||||
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrSerdeable, ReleaseDownloadBody,
|
||||
TaskName,
|
||||
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrReleaseDownloadBody,
|
||||
RadarrSerdeable, RadarrTaskName,
|
||||
},
|
||||
Serdeable,
|
||||
},
|
||||
@@ -304,7 +304,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_release_command() {
|
||||
let expected_release_download_body = ReleaseDownloadBody {
|
||||
let expected_release_download_body = RadarrReleaseDownloadBody {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
movie_id: 1,
|
||||
@@ -389,7 +389,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_start_task_command() {
|
||||
let expected_task_name = TaskName::ApplicationCheckUpdate;
|
||||
let expected_task_name = RadarrTaskName::ApplicationCheckUpdate;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
@@ -404,7 +404,7 @@ mod tests {
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let start_task_command = RadarrCommand::StartTask {
|
||||
task_name: TaskName::ApplicationCheckUpdate,
|
||||
task_name: RadarrTaskName::ApplicationCheckUpdate,
|
||||
};
|
||||
|
||||
let result = RadarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
|
||||
|
||||
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
execute_network_event,
|
||||
network::{radarr_network::RadarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
@@ -19,7 +18,7 @@ mod refresh_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum RadarrRefreshCommand {
|
||||
#[command(about = "Refresh all movie data for all movies in your library")]
|
||||
#[command(about = "Refresh all movie data for all movies in your Radarr library")]
|
||||
AllMovies,
|
||||
#[command(about = "Refresh movie data and scan disk for the movie with the given ID")]
|
||||
Movie {
|
||||
@@ -63,22 +62,38 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand>
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<()> {
|
||||
match self.command {
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
RadarrRefreshCommand::AllMovies => {
|
||||
execute_network_event!(self, RadarrEvent::UpdateAllMovies);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::UpdateAllMovies.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrRefreshCommand::Collections => {
|
||||
execute_network_event!(self, RadarrEvent::UpdateCollections);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::UpdateCollections.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrRefreshCommand::Downloads => {
|
||||
execute_network_event!(self, RadarrEvent::UpdateDownloads);
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::UpdateDownloads.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrRefreshCommand::Movie { movie_id } => {
|
||||
execute_network_event!(self, RadarrEvent::UpdateAndScan(Some(movie_id)));
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::UpdateAndScan(Some(movie_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ mod tests {
|
||||
use crate::cli::radarr::RadarrCommand;
|
||||
use crate::cli::Command;
|
||||
use crate::Cli;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_radarr_refresh_command_from() {
|
||||
@@ -81,7 +82,7 @@ mod tests {
|
||||
#[case(RadarrRefreshCommand::Collections, RadarrEvent::UpdateCollections)]
|
||||
#[case(RadarrRefreshCommand::Downloads, RadarrEvent::UpdateDownloads)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_blocklist_command(
|
||||
async fn test_handle_refresh_command(
|
||||
#[case] refresh_command: RadarrRefreshCommand,
|
||||
#[case] expected_radarr_event: RadarrEvent,
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::sonarr_models::{AddSeriesBody, AddSeriesOptions, SeriesMonitor, SeriesType},
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "add_command_handler_tests.rs"]
|
||||
mod add_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrAddCommand {
|
||||
#[command(about = "Add a new series to your Sonarr library")]
|
||||
Series {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The TVDB ID of the series you wish to add to your library",
|
||||
required = true
|
||||
)]
|
||||
tvdb_id: i64,
|
||||
#[arg(long, help = "The title of the series", required = true)]
|
||||
title: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all series data and metadata should live",
|
||||
required = true
|
||||
)]
|
||||
root_folder_path: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the quality profile to use for this series",
|
||||
required = true
|
||||
)]
|
||||
quality_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the language profile to use for this series",
|
||||
required = true
|
||||
)]
|
||||
language_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The type of series",
|
||||
value_enum,
|
||||
default_value_t = SeriesType::default()
|
||||
)]
|
||||
series_type: SeriesType,
|
||||
#[arg(long, help = "Disable monitoring for this series")]
|
||||
disable_monitoring: bool,
|
||||
#[arg(long, help = "Don't use season folders for this series")]
|
||||
disable_season_folders: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag the series with",
|
||||
value_parser,
|
||||
action = ArgAction::Append
|
||||
)]
|
||||
tag: Vec<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "What Sonarr should monitor",
|
||||
value_enum,
|
||||
default_value_t = SeriesMonitor::default()
|
||||
)]
|
||||
monitor: SeriesMonitor,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tell Sonarr to not start a search for this series once it's added to your library"
|
||||
)]
|
||||
no_search_for_series: bool,
|
||||
},
|
||||
#[command(about = "Add a new root folder")]
|
||||
RootFolder {
|
||||
#[arg(long, help = "The path of the new root folder", required = true)]
|
||||
root_folder_path: String,
|
||||
},
|
||||
#[command(about = "Add new tag")]
|
||||
Tag {
|
||||
#[arg(long, help = "The name of the tag to be added", required = true)]
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SonarrAddCommand> for Command {
|
||||
fn from(value: SonarrAddCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::Add(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrAddCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrAddCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrAddCommand::Series {
|
||||
tvdb_id,
|
||||
title,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
language_profile_id,
|
||||
series_type,
|
||||
disable_monitoring,
|
||||
disable_season_folders,
|
||||
tag: tags,
|
||||
monitor,
|
||||
no_search_for_series,
|
||||
} => {
|
||||
let body = AddSeriesBody {
|
||||
tvdb_id,
|
||||
title,
|
||||
monitored: !disable_monitoring,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
language_profile_id,
|
||||
series_type: series_type.to_string(),
|
||||
season_folder: !disable_season_folders,
|
||||
tags,
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: monitor.to_string(),
|
||||
search_for_cutoff_unmet_episodes: !no_search_for_series,
|
||||
search_for_missing_episodes: !no_search_for_series,
|
||||
},
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::AddSeries(Some(body)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrAddCommand::RootFolder { root_folder_path } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::AddRootFolder(Some(root_folder_path)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrAddCommand::Tag { name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::AddTag(name).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
sonarr::{add_command_handler::SonarrAddCommand, SonarrCommand},
|
||||
Command,
|
||||
},
|
||||
Cli,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_add_command_from() {
|
||||
let command = SonarrAddCommand::Tag {
|
||||
name: String::new(),
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Sonarr(SonarrCommand::Add(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use crate::models::sonarr_models::{SeriesMonitor, SeriesType};
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_add_root_folder_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "root-folder"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_root_folder_success() {
|
||||
let expected_args = SonarrAddCommand::RootFolder {
|
||||
root_folder_path: "/nfs/test".to_owned(),
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"root-folder",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "series"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_requires_tvdb_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--root-folder-path",
|
||||
"test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_requires_title() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_requires_root_folder_path() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_requires_quality_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"test",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_requires_language_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_add_series_assert_argument_flags_require_args(
|
||||
#[values("--series-type", "--tag", "--monitor")] flag: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
"--root-folder-path",
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
flag,
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_all_arguments_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--title",
|
||||
"test",
|
||||
"--root-folder-path",
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_series_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--root-folder-path",
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
"--series-type",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_monitor_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--root-folder-path",
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--tvdb-id",
|
||||
"--title",
|
||||
"test",
|
||||
"1",
|
||||
"--monitor",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_defaults() {
|
||||
let expected_args = SonarrAddCommand::Series {
|
||||
tvdb_id: 1,
|
||||
title: "test".to_owned(),
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
language_profile_id: 1,
|
||||
series_type: SeriesType::default(),
|
||||
disable_monitoring: false,
|
||||
disable_season_folders: false,
|
||||
tag: vec![],
|
||||
monitor: SeriesMonitor::default(),
|
||||
no_search_for_series: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--root-folder-path",
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_tags_is_repeatable() {
|
||||
let expected_args = SonarrAddCommand::Series {
|
||||
tvdb_id: 1,
|
||||
title: "test".to_owned(),
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
language_profile_id: 1,
|
||||
series_type: SeriesType::default(),
|
||||
disable_monitoring: false,
|
||||
disable_season_folders: false,
|
||||
tag: vec![1, 2],
|
||||
monitor: SeriesMonitor::default(),
|
||||
no_search_for_series: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--root-folder-path",
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_series_all_args_defined() {
|
||||
let expected_args = SonarrAddCommand::Series {
|
||||
tvdb_id: 1,
|
||||
title: "test".to_owned(),
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
language_profile_id: 1,
|
||||
series_type: SeriesType::Anime,
|
||||
disable_monitoring: true,
|
||||
disable_season_folders: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: SeriesMonitor::Future,
|
||||
no_search_for_series: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"add",
|
||||
"series",
|
||||
"--root-folder-path",
|
||||
"/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--series-type",
|
||||
"anime",
|
||||
"--disable-monitoring",
|
||||
"--disable-season-folders",
|
||||
"--tvdb-id",
|
||||
"1",
|
||||
"--title",
|
||||
"test",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
"--monitor",
|
||||
"future",
|
||||
"--no-search-for-series",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "tag"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_success() {
|
||||
let expected_args = SonarrAddCommand::Tag {
|
||||
name: "test".to_owned(),
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "sonarr", "add", "tag", "--name", "test"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{sonarr::add_command_handler::SonarrAddCommandHandler, CliCommandHandler},
|
||||
models::{
|
||||
sonarr_models::{
|
||||
AddSeriesBody, AddSeriesOptions, SeriesMonitor, SeriesType, SonarrSerdeable,
|
||||
},
|
||||
Serdeable,
|
||||
},
|
||||
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use mockall::predicate::eq;
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_root_folder_command() {
|
||||
let expected_root_folder_path = "/nfs/test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let add_root_folder_command = SonarrAddCommand::RootFolder {
|
||||
root_folder_path: expected_root_folder_path,
|
||||
};
|
||||
|
||||
let result =
|
||||
SonarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_series_command() {
|
||||
let expected_add_series_body = AddSeriesBody {
|
||||
tvdb_id: 1,
|
||||
title: "test".to_owned(),
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
language_profile_id: 1,
|
||||
series_type: "anime".to_owned(),
|
||||
monitored: false,
|
||||
tags: vec![1, 2],
|
||||
season_folder: false,
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: "future".to_owned(),
|
||||
search_for_cutoff_unmet_episodes: false,
|
||||
search_for_missing_episodes: false,
|
||||
},
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::AddSeries(Some(expected_add_series_body)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let add_series_command = SonarrAddCommand::Series {
|
||||
tvdb_id: 1,
|
||||
title: "test".to_owned(),
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
language_profile_id: 1,
|
||||
series_type: SeriesType::Anime,
|
||||
disable_monitoring: true,
|
||||
disable_season_folders: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: SeriesMonitor::Future,
|
||||
no_search_for_series: true,
|
||||
};
|
||||
|
||||
let result = SonarrAddCommandHandler::with(&app_arc, add_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_tag_command() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let add_tag_command = SonarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
};
|
||||
|
||||
let result = SonarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::sonarr_models::DeleteSeriesParams,
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_command_handler_tests.rs"]
|
||||
mod delete_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrDeleteCommand {
|
||||
#[command(about = "Delete the specified item from the Sonarr blocklist")]
|
||||
BlocklistItem {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the blocklist item to remove from the blocklist",
|
||||
required = true
|
||||
)]
|
||||
blocklist_item_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the specified download")]
|
||||
Download {
|
||||
#[arg(long, help = "The ID of the download to delete", required = true)]
|
||||
download_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the specified episode file from disk")]
|
||||
EpisodeFile {
|
||||
#[arg(long, help = "The ID of the episode file to delete", required = true)]
|
||||
episode_file_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the indexer with the given ID")]
|
||||
Indexer {
|
||||
#[arg(long, help = "The ID of the indexer to delete", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the root folder with the given ID")]
|
||||
RootFolder {
|
||||
#[arg(long, help = "The ID of the root folder to delete", required = true)]
|
||||
root_folder_id: i64,
|
||||
},
|
||||
#[command(about = "Delete a series from your Sonarr library")]
|
||||
Series {
|
||||
#[arg(long, help = "The ID of the series to delete", required = true)]
|
||||
series_id: i64,
|
||||
#[arg(long, help = "Delete the series files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this series")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the tag with the specified ID")]
|
||||
Tag {
|
||||
#[arg(long, help = "The ID of the tag to delete", required = true)]
|
||||
tag_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SonarrDeleteCommand> for Command {
|
||||
fn from(value: SonarrDeleteCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::Delete(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrDeleteCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrDeleteCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let resp = match self.command {
|
||||
SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrDeleteCommand::Download { download_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DeleteDownload(Some(download_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrDeleteCommand::EpisodeFile { episode_file_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DeleteEpisodeFile(Some(episode_file_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrDeleteCommand::Indexer { indexer_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DeleteIndexer(Some(indexer_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrDeleteCommand::RootFolder { root_folder_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DeleteRootFolder(Some(root_folder_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrDeleteCommand::Series {
|
||||
series_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_series_params = DeleteSeriesParams {
|
||||
id: series_id,
|
||||
delete_series_files: delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DeleteSeries(Some(delete_series_params)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrDeleteCommand::Tag { tag_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DeleteTag(tag_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
cli::{
|
||||
sonarr::{delete_command_handler::SonarrDeleteCommand, SonarrCommand},
|
||||
Command,
|
||||
},
|
||||
Cli,
|
||||
};
|
||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_delete_command_from() {
|
||||
let command = SonarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 1,
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Sonarr(SonarrCommand::Delete(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "blocklist-item"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_success() {
|
||||
let expected_args = SonarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 1,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"delete",
|
||||
"blocklist-item",
|
||||
"--blocklist-item-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "download"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_success() {
|
||||
let expected_args = SonarrDeleteCommand::Download { download_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"delete",
|
||||
"download",
|
||||
"--download-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_episode_file_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "episode-file"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_episode_file_success() {
|
||||
let expected_args = SonarrDeleteCommand::EpisodeFile { episode_file_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"delete",
|
||||
"episode-file",
|
||||
"--episode-file-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "indexer"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_success() {
|
||||
let expected_args = SonarrDeleteCommand::Indexer { indexer_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"delete",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "root-folder"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_success() {
|
||||
let expected_args = SonarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"delete",
|
||||
"root-folder",
|
||||
"--root-folder-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_series_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "series"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_series_defaults() {
|
||||
let expected_args = SonarrDeleteCommand::Series {
|
||||
series_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "sonarr", "delete", "series", "--series-id", "1"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_series_all_args_defined() {
|
||||
let expected_args = SonarrDeleteCommand::Series {
|
||||
series_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"delete",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "tag"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_success() {
|
||||
let expected_args = SonarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "sonarr", "delete", "tag", "--tag-id", "1"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
sonarr::delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler},
|
||||
CliCommandHandler,
|
||||
},
|
||||
models::{
|
||||
sonarr_models::{DeleteSeriesParams, SonarrSerdeable},
|
||||
Serdeable,
|
||||
},
|
||||
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_blocklist_item_command() {
|
||||
let expected_blocklist_item_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let delete_blocklist_item_command = SonarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 1,
|
||||
};
|
||||
|
||||
let result = SonarrDeleteCommandHandler::with(
|
||||
&app_arc,
|
||||
delete_blocklist_item_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_download_command() {
|
||||
let expected_download_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DeleteDownload(Some(expected_download_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let delete_download_command = SonarrDeleteCommand::Download { download_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_indexer_command() {
|
||||
let expected_indexer_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DeleteIndexer(Some(expected_indexer_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let delete_indexer_command = SonarrDeleteCommand::Indexer { indexer_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_root_folder_command() {
|
||||
let expected_root_folder_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DeleteRootFolder(Some(expected_root_folder_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let delete_root_folder_command = SonarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_series_command() {
|
||||
let expected_delete_series_params = DeleteSeriesParams {
|
||||
id: 1,
|
||||
delete_series_files: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DeleteSeries(Some(expected_delete_series_params)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let delete_series_command = SonarrDeleteCommand::Series {
|
||||
series_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result =
|
||||
SonarrDeleteCommandHandler::with(&app_arc, delete_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_tag_command() {
|
||||
let expected_tag_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DeleteTag(expected_tag_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let delete_tag_command = SonarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::sonarr_models::SonarrReleaseDownloadBody,
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "download_command_handler_tests.rs"]
|
||||
mod download_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrDownloadCommand {
|
||||
#[command(about = "Manually download the given series release for the specified series ID")]
|
||||
Series {
|
||||
#[arg(long, help = "The GUID of the release to download", required = true)]
|
||||
guid: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The indexer ID to download the release from",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The series ID that the release is associated with",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Manually download the given season release corresponding to the series specified with the series ID"
|
||||
)]
|
||||
Season {
|
||||
#[arg(long, help = "The GUID of the release to download", required = true)]
|
||||
guid: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The indexer ID to download the release from",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The series ID that the release is associated with",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The season number that the release corresponds to",
|
||||
required = true
|
||||
)]
|
||||
season_number: i64,
|
||||
},
|
||||
#[command(about = "Manually download the given episode release for the specified episode ID")]
|
||||
Episode {
|
||||
#[arg(long, help = "The GUID of the release to download", required = true)]
|
||||
guid: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The indexer ID to download the release from",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The episode ID that the release is associated with",
|
||||
required = true
|
||||
)]
|
||||
episode_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SonarrDownloadCommand> for Command {
|
||||
fn from(value: SonarrDownloadCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::Download(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrDownloadCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrDownloadCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDownloadCommand>
|
||||
for SonarrDownloadCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrDownloadCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrDownloadCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrDownloadCommand::Series {
|
||||
guid,
|
||||
indexer_id,
|
||||
series_id,
|
||||
} => {
|
||||
let params = SonarrReleaseDownloadBody {
|
||||
guid,
|
||||
indexer_id,
|
||||
series_id: Some(series_id),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrDownloadCommand::Season {
|
||||
guid,
|
||||
indexer_id,
|
||||
series_id,
|
||||
season_number,
|
||||
} => {
|
||||
let params = SonarrReleaseDownloadBody {
|
||||
guid,
|
||||
indexer_id,
|
||||
series_id: Some(series_id),
|
||||
season_number: Some(season_number),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrDownloadCommand::Episode {
|
||||
guid,
|
||||
indexer_id,
|
||||
episode_id,
|
||||
} => {
|
||||
let params = SonarrReleaseDownloadBody {
|
||||
guid,
|
||||
indexer_id,
|
||||
episode_id: Some(episode_id),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
cli::{
|
||||
sonarr::{download_command_handler::SonarrDownloadCommand, SonarrCommand},
|
||||
Command,
|
||||
},
|
||||
Cli,
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_download_command_from() {
|
||||
let command = SonarrDownloadCommand::Series {
|
||||
guid: "Test".to_owned(),
|
||||
indexer_id: 1,
|
||||
series_id: 1,
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Sonarr(SonarrCommand::Download(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_download_series_requires_series_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"series",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--guid",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_series_requires_guid() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"series",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_series_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"series",
|
||||
"--guid",
|
||||
"1",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_series_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"series",
|
||||
"--guid",
|
||||
"1",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_season_requires_series_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"season",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--season-number",
|
||||
"1",
|
||||
"--guid",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_season_requires_season_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"season",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--guid",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_season_requires_guid() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"season",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--season-number",
|
||||
"1",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_season_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"season",
|
||||
"--guid",
|
||||
"1",
|
||||
"--season-number",
|
||||
"1",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_season_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"season",
|
||||
"--guid",
|
||||
"1",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--season-number",
|
||||
"1",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_episode_requires_episode_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"episode",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--guid",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_episode_requires_guid() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"episode",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--episode-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_episode_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"episode",
|
||||
"--guid",
|
||||
"1",
|
||||
"--episode-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_episode_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"download",
|
||||
"episode",
|
||||
"--guid",
|
||||
"1",
|
||||
"--episode-id",
|
||||
"1",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
sonarr::download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler},
|
||||
CliCommandHandler,
|
||||
},
|
||||
models::{
|
||||
sonarr_models::{SonarrReleaseDownloadBody, SonarrSerdeable},
|
||||
Serdeable,
|
||||
},
|
||||
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_series_release_command() {
|
||||
let expected_release_download_body = SonarrReleaseDownloadBody {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
series_id: Some(1),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let download_release_command = SonarrDownloadCommand::Series {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
series_id: 1,
|
||||
};
|
||||
|
||||
let result =
|
||||
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_season_release_command() {
|
||||
let expected_release_download_body = SonarrReleaseDownloadBody {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
series_id: Some(1),
|
||||
season_number: Some(1),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let download_release_command = SonarrDownloadCommand::Season {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
series_id: 1,
|
||||
season_number: 1,
|
||||
};
|
||||
|
||||
let result =
|
||||
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_episode_release_command() {
|
||||
let expected_release_download_body = SonarrReleaseDownloadBody {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
episode_id: Some(1),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let download_release_command = SonarrDownloadCommand::Episode {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
episode_id: 1,
|
||||
};
|
||||
|
||||
let result =
|
||||
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, ArgGroup, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{mutex_flags_or_option, CliCommandHandler, Command},
|
||||
models::{
|
||||
servarr_models::EditIndexerParams,
|
||||
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
|
||||
Serdeable,
|
||||
},
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_command_handler_tests.rs"]
|
||||
mod edit_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrEditCommand {
|
||||
#[command(
|
||||
about = "Edit and indexer settings that apply to all indexers",
|
||||
group(
|
||||
ArgGroup::new("edit_settings")
|
||||
.args([
|
||||
"maximum_size",
|
||||
"minimum_age",
|
||||
"retention",
|
||||
"rss_sync_interval",
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
AllIndexerSettings {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
|
||||
)]
|
||||
maximum_size: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
|
||||
)]
|
||||
minimum_age: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
|
||||
)]
|
||||
retention: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
|
||||
)]
|
||||
rss_sync_interval: Option<i64>,
|
||||
},
|
||||
#[command(
|
||||
about = "Edit preferences for the specified indexer",
|
||||
group(
|
||||
ArgGroup::new("edit_indexer")
|
||||
.args([
|
||||
"name",
|
||||
"enable_rss",
|
||||
"disable_rss",
|
||||
"enable_automatic_search",
|
||||
"disable_automatic_search",
|
||||
"enable_interactive_search",
|
||||
"disable_automatic_search",
|
||||
"url",
|
||||
"api_key",
|
||||
"seed_ratio",
|
||||
"tag",
|
||||
"priority",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Indexer {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the indexer whose settings you wish to edit",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
#[arg(long, help = "The name of the indexer")]
|
||||
name: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Sonarr that this indexer should be used when Sonarr periodically looks for releases via RSS Sync",
|
||||
conflicts_with = "disable_rss"
|
||||
)]
|
||||
enable_rss: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer when Sonarr periodically looks for releases via RSS Sync",
|
||||
conflicts_with = "enable_rss"
|
||||
)]
|
||||
disable_rss: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Sonarr that this indexer should be used when automatic searches are performed via the UI or by Sonarr",
|
||||
conflicts_with = "disable_automatic_search"
|
||||
)]
|
||||
enable_automatic_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Sonarr",
|
||||
conflicts_with = "enable_automatic_search"
|
||||
)]
|
||||
disable_automatic_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Sonarr that this indexer should be used when an interactive search is used",
|
||||
conflicts_with = "disable_interactive_search"
|
||||
)]
|
||||
enable_interactive_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer whenever an interactive search is performed",
|
||||
conflicts_with = "enable_interactive_search"
|
||||
)]
|
||||
disable_interactive_search: bool,
|
||||
#[arg(long, help = "The URL of the indexer")]
|
||||
url: Option<String>,
|
||||
#[arg(long, help = "The API key used to access the indexer's API")]
|
||||
api_key: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
|
||||
)]
|
||||
seed_ratio: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Only use this indexer for series with at least one matching tag ID. Leave blank to use with all series.",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Sonarr will still use all enabled indexers for RSS Sync and Searching"
|
||||
)]
|
||||
priority: Option<i64>,
|
||||
#[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
#[command(
|
||||
about = "Edit preferences for the specified series",
|
||||
group(
|
||||
ArgGroup::new("edit_series")
|
||||
.args([
|
||||
"enable_monitoring",
|
||||
"disable_monitoring",
|
||||
"enable_season_folders",
|
||||
"disable_season_folders",
|
||||
"series_type",
|
||||
"quality_profile_id",
|
||||
"language_profile_id",
|
||||
"root_folder_path",
|
||||
"tag",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Series {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the series whose settings you want to edit",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Enable monitoring of this series in Sonarr so Sonarr will automatically download this series if it is available",
|
||||
conflicts_with = "disable_monitoring"
|
||||
)]
|
||||
enable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable monitoring of this series so Sonarr does not automatically download the series if it is found to be available",
|
||||
conflicts_with = "enable_monitoring"
|
||||
)]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The minimum availability to monitor for this film",
|
||||
value_enum
|
||||
)]
|
||||
#[arg(
|
||||
long,
|
||||
help = "Enable sorting episodes of this series into season folders",
|
||||
conflicts_with = "disable_season_folders"
|
||||
)]
|
||||
enable_season_folders: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable sorting episodes of this series into season folders",
|
||||
conflicts_with = "enable_season_folders"
|
||||
)]
|
||||
disable_season_folders: bool,
|
||||
#[arg(long, help = "The type of series", value_enum)]
|
||||
series_type: Option<SeriesType>,
|
||||
#[arg(long, help = "The ID of the quality profile to use for this series")]
|
||||
quality_profile_id: Option<i64>,
|
||||
#[arg(long, help = "The ID of the language profile to use for this series")]
|
||||
language_profile_id: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all film data and metadata should live"
|
||||
)]
|
||||
root_folder_path: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag this series with",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(long, help = "Clear all tags on this series", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SonarrEditCommand> for Command {
|
||||
fn from(value: SonarrEditCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::Edit(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrEditCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrEditCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrEditCommand::AllIndexerSettings {
|
||||
maximum_size,
|
||||
minimum_age,
|
||||
retention,
|
||||
rss_sync_interval,
|
||||
} => {
|
||||
if let Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(previous_indexer_settings)) = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetAllIndexerSettings.into())
|
||||
.await?
|
||||
{
|
||||
let params = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
|
||||
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
|
||||
retention: retention.unwrap_or(previous_indexer_settings.retention),
|
||||
rss_sync_interval: rss_sync_interval
|
||||
.unwrap_or(previous_indexer_settings.rss_sync_interval),
|
||||
};
|
||||
self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::EditAllIndexerSettings(Some(params)).into())
|
||||
.await?;
|
||||
"All indexer settings updated".to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
SonarrEditCommand::Indexer {
|
||||
indexer_id,
|
||||
name,
|
||||
enable_rss,
|
||||
disable_rss,
|
||||
enable_automatic_search,
|
||||
disable_automatic_search,
|
||||
enable_interactive_search,
|
||||
disable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tag,
|
||||
priority,
|
||||
clear_tags,
|
||||
} => {
|
||||
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
|
||||
let automatic_search_value =
|
||||
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
|
||||
let interactive_search_value =
|
||||
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
|
||||
let edit_indexer_params = EditIndexerParams {
|
||||
indexer_id,
|
||||
name,
|
||||
enable_rss: rss_value,
|
||||
enable_automatic_search: automatic_search_value,
|
||||
enable_interactive_search: interactive_search_value,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tags: tag,
|
||||
priority,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)).into())
|
||||
.await?;
|
||||
"Indexer updated".to_owned()
|
||||
}
|
||||
SonarrEditCommand::Series {
|
||||
series_id,
|
||||
enable_monitoring,
|
||||
disable_monitoring,
|
||||
enable_season_folders,
|
||||
disable_season_folders,
|
||||
series_type,
|
||||
quality_profile_id,
|
||||
language_profile_id,
|
||||
root_folder_path,
|
||||
tag,
|
||||
clear_tags,
|
||||
} => {
|
||||
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
|
||||
let season_folders_value =
|
||||
mutex_flags_or_option(enable_season_folders, disable_season_folders);
|
||||
let edit_series_params = EditSeriesParams {
|
||||
series_id,
|
||||
monitored: monitored_value,
|
||||
use_season_folders: season_folders_value,
|
||||
series_type,
|
||||
quality_profile_id,
|
||||
language_profile_id,
|
||||
root_folder_path,
|
||||
tags: tag,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::EditSeries(Some(edit_series_params)).into())
|
||||
.await?;
|
||||
"Series Updated".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,874 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
sonarr::{edit_command_handler::SonarrEditCommand, SonarrCommand},
|
||||
Command,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_edit_command_from() {
|
||||
let command = SonarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: None,
|
||||
minimum_age: None,
|
||||
retention: None,
|
||||
rss_sync_interval: None,
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Sonarr(SonarrCommand::Edit(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use crate::{models::sonarr_models::SeriesType, Cli};
|
||||
|
||||
use super::*;
|
||||
use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_edit_all_indexer_settings_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "all-indexer-settings"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_all_indexer_settings_assert_argument_flags_require_args(
|
||||
#[values(
|
||||
"--maximum-size",
|
||||
"--minimum-age",
|
||||
"--retention",
|
||||
"--rss-sync-interval"
|
||||
)]
|
||||
flag: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"all-indexer-settings",
|
||||
flag,
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() {
|
||||
let expected_args = SonarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: None,
|
||||
retention: None,
|
||||
rss_sync_interval: None,
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"all-indexer-settings",
|
||||
"--maximum-size",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_all_indexer_settings_all_arguments_defined() {
|
||||
let expected_args = SonarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"all-indexer-settings",
|
||||
"--maximum-size",
|
||||
"1",
|
||||
"--minimum-age",
|
||||
"1",
|
||||
"--retention",
|
||||
"1",
|
||||
"--rss-sync-interval",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "indexer"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_with_indexer_id_still_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_rss_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--enable-rss",
|
||||
"--disable-rss",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_automatic_search_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--enable-automatic-search",
|
||||
"--disable-automatic-search",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_interactive_search_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--enable-interactive-search",
|
||||
"--disable-interactive-search",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_tag_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--clear-tags",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_assert_argument_flags_require_args(
|
||||
#[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
flag,
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() {
|
||||
let expected_args = SonarrEditCommand::Indexer {
|
||||
indexer_id: 1,
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: false,
|
||||
disable_rss: false,
|
||||
enable_automatic_search: false,
|
||||
disable_automatic_search: false,
|
||||
enable_interactive_search: false,
|
||||
disable_interactive_search: false,
|
||||
url: None,
|
||||
api_key: None,
|
||||
seed_ratio: None,
|
||||
tag: None,
|
||||
priority: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--name",
|
||||
"Test",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_tag_argument_is_repeatable() {
|
||||
let expected_args = SonarrEditCommand::Indexer {
|
||||
indexer_id: 1,
|
||||
name: None,
|
||||
enable_rss: false,
|
||||
disable_rss: false,
|
||||
enable_automatic_search: false,
|
||||
disable_automatic_search: false,
|
||||
enable_interactive_search: false,
|
||||
disable_interactive_search: false,
|
||||
url: None,
|
||||
api_key: None,
|
||||
seed_ratio: None,
|
||||
tag: Some(vec![1, 2]),
|
||||
priority: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_all_arguments_defined() {
|
||||
let expected_args = SonarrEditCommand::Indexer {
|
||||
indexer_id: 1,
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: true,
|
||||
disable_rss: false,
|
||||
enable_automatic_search: true,
|
||||
disable_automatic_search: false,
|
||||
enable_interactive_search: true,
|
||||
disable_interactive_search: false,
|
||||
url: Some("http://test.com".to_owned()),
|
||||
api_key: Some("testKey".to_owned()),
|
||||
seed_ratio: Some("1.2".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
priority: Some(25),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--name",
|
||||
"Test",
|
||||
"--enable-rss",
|
||||
"--enable-automatic-search",
|
||||
"--enable-interactive-search",
|
||||
"--url",
|
||||
"http://test.com",
|
||||
"--api-key",
|
||||
"testKey",
|
||||
"--seed-ratio",
|
||||
"1.2",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
"--priority",
|
||||
"25",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "series"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_with_series_id_still_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_monitoring_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--enable-monitoring",
|
||||
"--disable-monitoring",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_season_folders_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--enable-season-folders",
|
||||
"--disable-season-folders",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_tag_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--clear-tags",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_series_assert_argument_flags_require_args(
|
||||
#[values(
|
||||
"--series-type",
|
||||
"--quality-profile-id",
|
||||
"--language-profile-id",
|
||||
"--root-folder-path",
|
||||
"--tag"
|
||||
)]
|
||||
flag: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
flag,
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_series_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--series-type",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_only_requires_at_least_one_argument_plus_series_id() {
|
||||
let expected_args = SonarrEditCommand::Series {
|
||||
series_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
enable_season_folders: false,
|
||||
disable_season_folders: false,
|
||||
series_type: None,
|
||||
quality_profile_id: None,
|
||||
language_profile_id: None,
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_tag_argument_is_repeatable() {
|
||||
let expected_args = SonarrEditCommand::Series {
|
||||
series_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
enable_season_folders: false,
|
||||
disable_season_folders: false,
|
||||
series_type: None,
|
||||
quality_profile_id: None,
|
||||
language_profile_id: None,
|
||||
root_folder_path: None,
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_series_all_arguments_defined() {
|
||||
let expected_args = SonarrEditCommand::Series {
|
||||
series_id: 1,
|
||||
enable_monitoring: true,
|
||||
disable_monitoring: false,
|
||||
enable_season_folders: true,
|
||||
disable_season_folders: false,
|
||||
series_type: Some(SeriesType::Anime),
|
||||
quality_profile_id: Some(1),
|
||||
language_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"edit",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--enable-monitoring",
|
||||
"--enable-season-folders",
|
||||
"--series-type",
|
||||
"anime",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--language-profile-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
sonarr::edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler},
|
||||
CliCommandHandler,
|
||||
},
|
||||
models::{
|
||||
servarr_models::EditIndexerParams,
|
||||
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
|
||||
Serdeable,
|
||||
},
|
||||
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_all_indexer_settings_command() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
};
|
||||
|
||||
let result = SonarrEditCommandHandler::with(
|
||||
&app_arc,
|
||||
edit_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_indexer_command() {
|
||||
let expected_edit_indexer_params = EditIndexerParams {
|
||||
indexer_id: 1,
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: Some(true),
|
||||
enable_automatic_search: Some(true),
|
||||
enable_interactive_search: Some(true),
|
||||
url: Some("http://test.com".to_owned()),
|
||||
api_key: Some("testKey".to_owned()),
|
||||
seed_ratio: Some("1.2".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
priority: Some(25),
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let edit_indexer_command = SonarrEditCommand::Indexer {
|
||||
indexer_id: 1,
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: true,
|
||||
disable_rss: false,
|
||||
enable_automatic_search: true,
|
||||
disable_automatic_search: false,
|
||||
enable_interactive_search: true,
|
||||
disable_interactive_search: false,
|
||||
url: Some("http://test.com".to_owned()),
|
||||
api_key: Some("testKey".to_owned()),
|
||||
seed_ratio: Some("1.2".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
priority: Some(25),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
SonarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_series_command() {
|
||||
let expected_edit_series_params = EditSeriesParams {
|
||||
series_id: 1,
|
||||
monitored: Some(true),
|
||||
use_season_folders: Some(true),
|
||||
series_type: Some(SeriesType::Anime),
|
||||
quality_profile_id: Some(1),
|
||||
language_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let edit_series_command = SonarrEditCommand::Series {
|
||||
series_id: 1,
|
||||
enable_monitoring: true,
|
||||
disable_monitoring: false,
|
||||
enable_season_folders: true,
|
||||
disable_season_folders: false,
|
||||
series_type: Some(SeriesType::Anime),
|
||||
quality_profile_id: Some(1),
|
||||
language_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_series_command_handles_disable_monitoring_flag_properly() {
|
||||
let expected_edit_series_params = EditSeriesParams {
|
||||
series_id: 1,
|
||||
monitored: Some(false),
|
||||
use_season_folders: Some(false),
|
||||
series_type: Some(SeriesType::Anime),
|
||||
quality_profile_id: Some(1),
|
||||
language_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let edit_series_command = SonarrEditCommand::Series {
|
||||
series_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: true,
|
||||
enable_season_folders: false,
|
||||
disable_season_folders: true,
|
||||
series_type: Some(SeriesType::Anime),
|
||||
quality_profile_id: Some(1),
|
||||
language_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_series_command_no_monitoring_boolean_flags_returns_none_value() {
|
||||
let expected_edit_series_params = EditSeriesParams {
|
||||
series_id: 1,
|
||||
monitored: None,
|
||||
use_season_folders: None,
|
||||
series_type: Some(SeriesType::Anime),
|
||||
quality_profile_id: Some(1),
|
||||
language_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let edit_series_command = SonarrEditCommand::Series {
|
||||
series_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
enable_season_folders: false,
|
||||
disable_season_folders: false,
|
||||
series_type: Some(SeriesType::Anime),
|
||||
quality_profile_id: Some(1),
|
||||
language_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "get_command_handler_tests.rs"]
|
||||
mod get_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrGetCommand {
|
||||
#[command(about = "Get the shared settings for all indexers")]
|
||||
AllIndexerSettings,
|
||||
#[command(about = "Get detailed information for the episode with the given ID")]
|
||||
EpisodeDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the episode whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
episode_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch the host config for your Sonarr instance")]
|
||||
HostConfig,
|
||||
#[command(about = "Fetch the security config for your Sonarr instance")]
|
||||
SecurityConfig,
|
||||
#[command(about = "Get detailed information for the series with the given ID")]
|
||||
SeriesDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the series whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
},
|
||||
#[command(about = "Get the system status")]
|
||||
SystemStatus,
|
||||
}
|
||||
|
||||
impl From<SonarrGetCommand> for Command {
|
||||
fn from(value: SonarrGetCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::Get(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrGetCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrGetCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrGetCommand::AllIndexerSettings => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetAllIndexerSettings.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrGetCommand::EpisodeDetails { episode_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetEpisodeDetails(Some(episode_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrGetCommand::HostConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetHostConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrGetCommand::SecurityConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetSecurityConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrGetCommand::SeriesDetails { series_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetSeriesDetails(Some(series_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrGetCommand::SystemStatus => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetStatus.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
sonarr::{get_command_handler::SonarrGetCommand, SonarrCommand},
|
||||
Command,
|
||||
};
|
||||
use crate::Cli;
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_get_command_from() {
|
||||
let command = SonarrGetCommand::SystemStatus;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Sonarr(SonarrCommand::Get(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_all_indexer_settings_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "all-indexer-settings"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_status_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "system-status"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_episode_details_requires_episode_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "episode-details"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_episode_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"get",
|
||||
"episode-details",
|
||||
"--episode-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_host_config_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "host-config"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_security_config_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "security-config"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_series_details_requires_series_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "series-details"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_series_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"get",
|
||||
"series-details",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
sonarr::get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler},
|
||||
CliCommandHandler,
|
||||
},
|
||||
models::{sonarr_models::SonarrSerdeable, Serdeable},
|
||||
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_all_indexer_settings_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_all_indexer_settings_command = SonarrGetCommand::AllIndexerSettings;
|
||||
|
||||
let result = SonarrGetCommandHandler::with(
|
||||
&app_arc,
|
||||
get_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_episode_details_command() {
|
||||
let expected_episode_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetEpisodeDetails(Some(expected_episode_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_episode_details_command = SonarrGetCommand::EpisodeDetails { episode_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrGetCommandHandler::with(&app_arc, get_episode_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_host_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::GetHostConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_host_config_command = SonarrGetCommand::HostConfig;
|
||||
|
||||
let result =
|
||||
SonarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_security_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::GetSecurityConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_security_config_command = SonarrGetCommand::SecurityConfig;
|
||||
|
||||
let result =
|
||||
SonarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_series_details_command() {
|
||||
let expected_series_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetSeriesDetails(Some(expected_series_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_series_details_command = SonarrGetCommand::SeriesDetails { series_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrGetCommandHandler::with(&app_arc, get_series_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_system_status_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_system_status_command = SonarrGetCommand::SystemStatus;
|
||||
|
||||
let result =
|
||||
SonarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "list_command_handler_tests.rs"]
|
||||
mod list_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrListCommand {
|
||||
#[command(about = "List all items in the Sonarr blocklist")]
|
||||
Blocklist,
|
||||
#[command(about = "List all active downloads in Sonarr")]
|
||||
Downloads,
|
||||
#[command(about = "List disk space details for all provisioned root folders in Sonarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "List the episodes for the series with the given ID")]
|
||||
Episodes {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the series whose episodes you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch all history events for the episode with the given ID")]
|
||||
EpisodeHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the episode whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
episode_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch all Sonarr history events")]
|
||||
History {
|
||||
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
},
|
||||
#[command(about = "List all Sonarr indexers")]
|
||||
Indexers,
|
||||
#[command(about = "List all Sonarr language profiles")]
|
||||
LanguageProfiles,
|
||||
#[command(about = "Fetch Sonarr logs")]
|
||||
Logs {
|
||||
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Output the logs in the same format as they appear in the log files"
|
||||
)]
|
||||
output_in_log_format: bool,
|
||||
},
|
||||
#[command(about = "List all Sonarr quality profiles")]
|
||||
QualityProfiles,
|
||||
#[command(about = "List all queued events")]
|
||||
QueuedEvents,
|
||||
#[command(about = "List all root folders in Sonarr")]
|
||||
RootFolders,
|
||||
#[command(about = "List all series in your Sonarr library")]
|
||||
Series,
|
||||
#[command(about = "Fetch all history events for the series with the given ID")]
|
||||
SeriesHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the series whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
},
|
||||
#[command(about = "List all Sonarr tags")]
|
||||
Tags,
|
||||
#[command(about = "List all Sonarr tasks")]
|
||||
Tasks,
|
||||
#[command(about = "List all Sonarr updates")]
|
||||
Updates,
|
||||
}
|
||||
|
||||
impl From<SonarrListCommand> for Command {
|
||||
fn from(value: SonarrListCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::List(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrListCommandHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrListCommandHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrListCommand::Blocklist => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::Downloads => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetDownloads.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::DiskSpace => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetDiskSpace.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::Episodes { series_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetEpisodes(Some(series_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::EpisodeHistory { episode_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetEpisodeHistory(Some(episode_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::History { events: items } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetHistory(Some(items)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::Indexers => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::LanguageProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetLanguageProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::Logs {
|
||||
events,
|
||||
output_in_log_format,
|
||||
} => {
|
||||
let logs = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetLogs(Some(events)).into())
|
||||
.await?;
|
||||
|
||||
if output_in_log_format {
|
||||
let log_lines = self.app.lock().await.data.sonarr_data.logs.items.clone();
|
||||
|
||||
serde_json::to_string_pretty(&log_lines)?
|
||||
} else {
|
||||
serde_json::to_string_pretty(&logs)?
|
||||
}
|
||||
}
|
||||
SonarrListCommand::QualityProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetQualityProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::QueuedEvents => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetQueuedEvents.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::RootFolders => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetRootFolders.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::Series => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::ListSeries.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::SeriesHistory { series_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetSeriesHistory(Some(series_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::Tags => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetTags.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::Tasks => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetTasks.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrListCommand::Updates => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetUpdates.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
sonarr::{list_command_handler::SonarrListCommand, SonarrCommand},
|
||||
Command,
|
||||
};
|
||||
use crate::Cli;
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_list_command_from() {
|
||||
let command = SonarrListCommand::Series;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Sonarr(SonarrCommand::List(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{error::ErrorKind, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_list_commands_have_no_arg_requirements(
|
||||
#[values(
|
||||
"blocklist",
|
||||
"series",
|
||||
"downloads",
|
||||
"disk-space",
|
||||
"quality-profiles",
|
||||
"indexers",
|
||||
"queued-events",
|
||||
"root-folders",
|
||||
"tags",
|
||||
"tasks",
|
||||
"updates",
|
||||
"language-profiles"
|
||||
)]
|
||||
subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", subcommand]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_episodes_requires_series_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episodes"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_episode_history_requires_series_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episode-history"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_episode_history_success() {
|
||||
let expected_args = SonarrListCommand::EpisodeHistory { episode_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"list",
|
||||
"episode-history",
|
||||
"--episode-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::List(episode_history_command))) =
|
||||
result.unwrap().command
|
||||
{
|
||||
assert_eq!(episode_history_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "history", "--events"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_default_values() {
|
||||
let expected_args = SonarrListCommand::History { events: 500 };
|
||||
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "history"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::List(history_command))) = result.unwrap().command {
|
||||
assert_eq!(history_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "logs", "--events"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_default_values() {
|
||||
let expected_args = SonarrListCommand::Logs {
|
||||
events: 500,
|
||||
output_in_log_format: false,
|
||||
};
|
||||
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "logs"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::List(logs_command))) = result.unwrap().command {
|
||||
assert_eq!(logs_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_episodes_success() {
|
||||
let expected_args = SonarrListCommand::Episodes { series_id: 1 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "sonarr", "list", "episodes", "--series-id", "1"]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::List(episodes_command))) = result.unwrap().command
|
||||
{
|
||||
assert_eq!(episodes_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_series_history_requires_series_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series-history"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_series_history_success() {
|
||||
let expected_args = SonarrListCommand::SeriesHistory { series_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"list",
|
||||
"series-history",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::List(series_command))) = result.unwrap().command {
|
||||
assert_eq!(series_command, expected_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::sonarr::list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::models::sonarr_models::SonarrSerdeable;
|
||||
use crate::models::Serdeable;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[case(SonarrListCommand::Blocklist, SonarrEvent::GetBlocklist)]
|
||||
#[case(SonarrListCommand::Downloads, SonarrEvent::GetDownloads)]
|
||||
#[case(SonarrListCommand::DiskSpace, SonarrEvent::GetDiskSpace)]
|
||||
#[case(SonarrListCommand::Indexers, SonarrEvent::GetIndexers)]
|
||||
#[case(SonarrListCommand::QualityProfiles, SonarrEvent::GetQualityProfiles)]
|
||||
#[case(SonarrListCommand::QueuedEvents, SonarrEvent::GetQueuedEvents)]
|
||||
#[case(SonarrListCommand::RootFolders, SonarrEvent::GetRootFolders)]
|
||||
#[case(SonarrListCommand::Series, SonarrEvent::ListSeries)]
|
||||
#[case(SonarrListCommand::Tags, SonarrEvent::GetTags)]
|
||||
#[case(SonarrListCommand::Tasks, SonarrEvent::GetTasks)]
|
||||
#[case(SonarrListCommand::Updates, SonarrEvent::GetUpdates)]
|
||||
#[case(SonarrListCommand::LanguageProfiles, SonarrEvent::GetLanguageProfiles)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_command(
|
||||
#[case] list_command: SonarrListCommand,
|
||||
#[case] expected_sonarr_event: SonarrEvent,
|
||||
) {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
|
||||
let result = SonarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_episodes_command() {
|
||||
let expected_series_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetEpisodes(Some(expected_series_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let list_episodes_command = SonarrListCommand::Episodes { series_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrListCommandHandler::with(&app_arc, list_episodes_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_history_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetHistory(Some(expected_events)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let list_history_command = SonarrListCommand::History { events: 1000 };
|
||||
|
||||
let result =
|
||||
SonarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_logs_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetLogs(Some(expected_events)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let list_logs_command = SonarrListCommand::Logs {
|
||||
events: 1000,
|
||||
output_in_log_format: false,
|
||||
};
|
||||
|
||||
let result = SonarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_series_history_command() {
|
||||
let expected_series_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetSeriesHistory(Some(expected_series_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let list_series_history_command = SonarrListCommand::SeriesHistory { series_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrListCommandHandler::with(&app_arc, list_series_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_episode_history_command() {
|
||||
let expected_episode_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetEpisodeHistory(Some(expected_episode_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let list_episode_history_command = SonarrListCommand::EpisodeHistory { episode_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrListCommandHandler::with(&app_arc, list_episode_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "manual_search_command_handler_tests.rs"]
|
||||
mod manual_search_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrManualSearchCommand {
|
||||
#[command(about = "Trigger a manual search of releases for the episode with the given ID")]
|
||||
Episode {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the episode whose releases you wish to fetch and list",
|
||||
required = true
|
||||
)]
|
||||
episode_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID.\nNote that when downloading a season release, ensure that the release includes 'fullSeason: true', otherwise you'll run into issues"
|
||||
)]
|
||||
Season {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the series whose releases you wish to fetch and list",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
#[arg(long, help = "The season number to search for", required = true)]
|
||||
season_number: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SonarrManualSearchCommand> for Command {
|
||||
fn from(value: SonarrManualSearchCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::ManualSearch(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrManualSearchCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrManualSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
|
||||
for SonarrManualSearchCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrManualSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrManualSearchCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrManualSearchCommand::Episode { episode_id } => {
|
||||
println!("Searching for episode releases. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetEpisodeReleases(Some(episode_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrManualSearchCommand::Season {
|
||||
series_id,
|
||||
season_number,
|
||||
} => {
|
||||
println!("Searching for season releases. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(
|
||||
SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(),
|
||||
)
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
sonarr::{manual_search_command_handler::SonarrManualSearchCommand, SonarrCommand},
|
||||
Command,
|
||||
};
|
||||
use crate::Cli;
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_manual_search_command_from() {
|
||||
let command = SonarrManualSearchCommand::Episode { episode_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Command::Sonarr(SonarrCommand::ManualSearch(command))
|
||||
);
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_manual_season_search_requires_series_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"manual-search",
|
||||
"season",
|
||||
"--season-number",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_season_search_requires_season_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"manual-search",
|
||||
"season",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_season_search_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"manual-search",
|
||||
"season",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--season-number",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_episode_search_requires_episode_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "manual-search", "episode"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_episode_search_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"manual-search",
|
||||
"episode",
|
||||
"--episode-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
sonarr::manual_search_command_handler::{
|
||||
SonarrManualSearchCommand, SonarrManualSearchCommandHandler,
|
||||
},
|
||||
CliCommandHandler,
|
||||
},
|
||||
models::{sonarr_models::SonarrSerdeable, Serdeable},
|
||||
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manual_episode_search_command() {
|
||||
let expected_episode_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 };
|
||||
|
||||
let result = SonarrManualSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
manual_episode_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manual_season_search_command() {
|
||||
let expected_series_id = 1;
|
||||
let expected_season_number = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetSeasonReleases(Some((expected_series_id, expected_season_number))).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let manual_season_search_command = SonarrManualSearchCommand::Season {
|
||||
series_id: 1,
|
||||
season_number: 1,
|
||||
};
|
||||
|
||||
let result = SonarrManualSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
manual_season_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use add_command_handler::{SonarrAddCommand, SonarrAddCommandHandler};
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler};
|
||||
use download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler};
|
||||
use edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler};
|
||||
use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler};
|
||||
use list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
|
||||
use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler};
|
||||
use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler};
|
||||
use tokio::sync::Mutex;
|
||||
use trigger_automatic_search_command_handler::{
|
||||
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
models::sonarr_models::SonarrTaskName,
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::{CliCommandHandler, Command};
|
||||
|
||||
mod add_command_handler;
|
||||
mod delete_command_handler;
|
||||
mod download_command_handler;
|
||||
mod edit_command_handler;
|
||||
mod get_command_handler;
|
||||
mod list_command_handler;
|
||||
mod manual_search_command_handler;
|
||||
mod refresh_command_handler;
|
||||
mod trigger_automatic_search_command_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_command_tests.rs"]
|
||||
mod sonarr_command_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrCommand {
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to add or create new resources within your Sonarr instance"
|
||||
)]
|
||||
Add(SonarrAddCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to delete resources from your Sonarr instance"
|
||||
)]
|
||||
Delete(SonarrDeleteCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to edit resources in your Sonarr instance"
|
||||
)]
|
||||
Edit(SonarrEditCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to fetch details of the resources in your Sonarr instance"
|
||||
)]
|
||||
Get(SonarrGetCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to download releases in your Sonarr instance"
|
||||
)]
|
||||
Download(SonarrDownloadCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to list attributes from your Sonarr instance"
|
||||
)]
|
||||
List(SonarrListCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to refresh the data in your Sonarr instance"
|
||||
)]
|
||||
Refresh(SonarrRefreshCommand),
|
||||
#[command(subcommand, about = "Commands to manually search for releases")]
|
||||
ManualSearch(SonarrManualSearchCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to trigger automatic searches for releases of different resources in your Sonarr instance"
|
||||
)]
|
||||
TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand),
|
||||
#[command(about = "Clear the blocklist")]
|
||||
ClearBlocklist,
|
||||
#[command(about = "Mark the Sonarr history item with the given ID as 'failed'")]
|
||||
MarkHistoryItemAsFailed {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the history item you wish to mark as 'failed'",
|
||||
required = true
|
||||
)]
|
||||
history_item_id: i64,
|
||||
},
|
||||
#[command(about = "Search for a new series to add to Sonarr")]
|
||||
SearchNewSeries {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The title of the series you want to search for",
|
||||
required = true
|
||||
)]
|
||||
query: String,
|
||||
},
|
||||
#[command(about = "Start the specified Sonarr task")]
|
||||
StartTask {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The name of the task to trigger",
|
||||
value_enum,
|
||||
required = true
|
||||
)]
|
||||
task_name: SonarrTaskName,
|
||||
},
|
||||
#[command(
|
||||
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
|
||||
)]
|
||||
TestIndexer {
|
||||
#[arg(long, help = "The ID of the indexer to test", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Test all Sonarr indexers")]
|
||||
TestAllIndexers,
|
||||
}
|
||||
|
||||
impl From<SonarrCommand> for Command {
|
||||
fn from(sonarr_command: SonarrCommand) -> Command {
|
||||
Command::Sonarr(sonarr_command)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrCliHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrCliHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrCommand::Add(add_command) => {
|
||||
SonarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::Delete(delete_command) => {
|
||||
SonarrDeleteCommandHandler::with(self.app, delete_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::Edit(edit_command) => {
|
||||
SonarrEditCommandHandler::with(self.app, edit_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::Download(download_command) => {
|
||||
SonarrDownloadCommandHandler::with(self.app, download_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::Get(get_command) => {
|
||||
SonarrGetCommandHandler::with(self.app, get_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::List(list_command) => {
|
||||
SonarrListCommandHandler::with(self.app, list_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::Refresh(refresh_command) => {
|
||||
SonarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::ManualSearch(manual_search_command) => {
|
||||
SonarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
|
||||
SonarrTriggerAutomaticSearchCommandHandler::with(
|
||||
self.app,
|
||||
trigger_automatic_search_command,
|
||||
self.network,
|
||||
)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
SonarrCommand::ClearBlocklist => {
|
||||
self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::ClearBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
|
||||
let _ = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
|
||||
.await?;
|
||||
"Sonarr history item marked as 'failed'".to_owned()
|
||||
}
|
||||
SonarrCommand::SearchNewSeries { query } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::SearchNewSeries(Some(query)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrCommand::StartTask { task_name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::StartTask(Some(task_name)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrCommand::TestIndexer { indexer_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::TestIndexer(Some(indexer_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrCommand::TestAllIndexers => {
|
||||
println!("Testing all Sonarr indexers. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::TestAllIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "refresh_command_handler_tests.rs"]
|
||||
mod refresh_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrRefreshCommand {
|
||||
#[command(about = "Refresh all series data for all series in your Sonarr library")]
|
||||
AllSeries,
|
||||
#[command(about = "Refresh series data and scan disk for the series with the given ID")]
|
||||
Series {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the series to refresh information on and to scan the disk for",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
},
|
||||
#[command(about = "Refresh all downloads in Sonarr")]
|
||||
Downloads,
|
||||
}
|
||||
|
||||
impl From<SonarrRefreshCommand> for Command {
|
||||
fn from(value: SonarrRefreshCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::Refresh(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrRefreshCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrRefreshCommand>
|
||||
for SonarrRefreshCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrRefreshCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> anyhow::Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrRefreshCommand::AllSeries => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::UpdateAllSeries.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrRefreshCommand::Series { series_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::UpdateAndScanSeries(Some(series_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrRefreshCommand::Downloads => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::UpdateDownloads.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::cli::{
|
||||
sonarr::{refresh_command_handler::SonarrRefreshCommand, SonarrCommand},
|
||||
Command,
|
||||
};
|
||||
use crate::Cli;
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_refresh_command_from() {
|
||||
let command = SonarrRefreshCommand::AllSeries;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Sonarr(SonarrCommand::Refresh(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{error::ErrorKind, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_refresh_commands_have_no_arg_requirements(
|
||||
#[values("all-series", "downloads")] subcommand: &str,
|
||||
) {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "refresh", subcommand]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_series_requires_series_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "refresh", "series"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_series_success() {
|
||||
let expected_args = SonarrRefreshCommand::Series { series_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"refresh",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Some(Command::Sonarr(SonarrCommand::Refresh(refresh_command))) =
|
||||
result.unwrap().command
|
||||
{
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use rstest::rstest;
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{app::App, cli::sonarr::refresh_command_handler::SonarrRefreshCommandHandler};
|
||||
use crate::{
|
||||
cli::{sonarr::refresh_command_handler::SonarrRefreshCommand, CliCommandHandler},
|
||||
network::sonarr_network::SonarrEvent,
|
||||
};
|
||||
use crate::{
|
||||
models::{sonarr_models::SonarrSerdeable, Serdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[case(SonarrRefreshCommand::AllSeries, SonarrEvent::UpdateAllSeries)]
|
||||
#[case(SonarrRefreshCommand::Downloads, SonarrEvent::UpdateDownloads)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_command(
|
||||
#[case] refresh_command: SonarrRefreshCommand,
|
||||
#[case] expected_sonarr_event: SonarrEvent,
|
||||
) {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
|
||||
let result = SonarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_series_command() {
|
||||
let expected_series_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let refresh_series_command = SonarrRefreshCommand::Series { series_id: 1 };
|
||||
|
||||
let result =
|
||||
SonarrRefreshCommandHandler::with(&app_arc, refresh_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
sonarr::{list_command_handler::SonarrListCommand, SonarrCommand},
|
||||
Command,
|
||||
};
|
||||
use crate::Cli;
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_command_from() {
|
||||
let command = SonarrCommand::List(SonarrListCommand::Series);
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Sonarr(command));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_commands_that_have_no_arg_requirements(
|
||||
#[values("clear-blocklist", "test-all-indexers")] subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", subcommand]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requires_history_item_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "sonarr", "mark-history-item-as-failed"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"mark-history-item-as-failed",
|
||||
"--history-item-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_series_requires_query() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "search-new-series"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_series_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"search-new-series",
|
||||
"--query",
|
||||
"halo",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_requires_task_name() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "start-task"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_task_name_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"start-task",
|
||||
"--task-name",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"start-task",
|
||||
"--task-name",
|
||||
"application-update-check",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_indexer_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "test-indexer"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_indexer_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"test-indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
sonarr::{
|
||||
add_command_handler::SonarrAddCommand, delete_command_handler::SonarrDeleteCommand,
|
||||
download_command_handler::SonarrDownloadCommand, edit_command_handler::SonarrEditCommand,
|
||||
get_command_handler::SonarrGetCommand, list_command_handler::SonarrListCommand,
|
||||
manual_search_command_handler::SonarrManualSearchCommand,
|
||||
refresh_command_handler::SonarrRefreshCommand,
|
||||
trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand,
|
||||
SonarrCliHandler, SonarrCommand,
|
||||
},
|
||||
CliCommandHandler,
|
||||
},
|
||||
models::{
|
||||
sonarr_models::{
|
||||
BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody,
|
||||
SonarrSerdeable, SonarrTaskName,
|
||||
},
|
||||
Serdeable,
|
||||
},
|
||||
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_clear_blocklist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::BlocklistResponse(
|
||||
BlocklistResponse {
|
||||
records: vec![BlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let claer_blocklist_command = SonarrCommand::ClearBlocklist;
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_history_item_as_failed_command() {
|
||||
let expected_history_item_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let mark_history_item_as_failed_command =
|
||||
SonarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 };
|
||||
|
||||
let result = SonarrCliHandler::with(
|
||||
&app_arc,
|
||||
mark_history_item_as_failed_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let add_tag_command = SonarrCommand::Add(SonarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
});
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
|
||||
let expected_blocklist_item_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let delete_blocklist_item_command =
|
||||
SonarrCommand::Delete(SonarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 1,
|
||||
});
|
||||
|
||||
let result =
|
||||
SonarrCliHandler::with(&app_arc, delete_blocklist_item_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_download_commands_to_the_download_command_handler() {
|
||||
let expected_params = SonarrReleaseDownloadBody {
|
||||
guid: "1234".to_owned(),
|
||||
indexer_id: 1,
|
||||
series_id: Some(1),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::DownloadRelease(expected_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let download_series_release_command =
|
||||
SonarrCommand::Download(SonarrDownloadCommand::Series {
|
||||
guid: "1234".to_owned(),
|
||||
indexer_id: 1,
|
||||
series_id: 1,
|
||||
});
|
||||
|
||||
let result =
|
||||
SonarrCliHandler::with(&app_arc, download_series_release_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let edit_all_indexer_settings_command =
|
||||
SonarrCommand::Edit(SonarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
});
|
||||
|
||||
let result = SonarrCliHandler::with(
|
||||
&app_arc,
|
||||
edit_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler(
|
||||
) {
|
||||
let expected_episode_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let manual_episode_search_command =
|
||||
SonarrCommand::ManualSearch(SonarrManualSearchCommand::Episode { episode_id: 1 });
|
||||
|
||||
let result =
|
||||
SonarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler(
|
||||
) {
|
||||
let expected_episode_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let manual_episode_search_command =
|
||||
SonarrCommand::TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand::Episode {
|
||||
episode_id: 1,
|
||||
});
|
||||
|
||||
let result =
|
||||
SonarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let get_system_status_command = SonarrCommand::Get(SonarrGetCommand::SystemStatus);
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::ListSeries.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::SeriesVec(vec![
|
||||
Series::default(),
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let list_series_command = SonarrCommand::List(SonarrListCommand::Series);
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, list_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sonarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
|
||||
let expected_series_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let refresh_series_command =
|
||||
SonarrCommand::Refresh(SonarrRefreshCommand::Series { series_id: 1 });
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, refresh_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_new_series_command() {
|
||||
let expected_search_query = "halo".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::SearchNewSeries(Some(expected_search_query)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let search_new_series_command = SonarrCommand::SearchNewSeries {
|
||||
query: "halo".to_owned(),
|
||||
};
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, search_new_series_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_start_task_command() {
|
||||
let expected_task_name = SonarrTaskName::ApplicationUpdateCheck;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::StartTask(Some(expected_task_name)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let start_task_command = SonarrCommand::StartTask {
|
||||
task_name: SonarrTaskName::ApplicationUpdateCheck,
|
||||
};
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_test_indexer_command() {
|
||||
let expected_indexer_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::TestIndexer(Some(expected_indexer_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let test_indexer_command = SonarrCommand::TestIndexer { indexer_id: 1 };
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_test_all_indexers_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(SonarrEvent::TestAllIndexers.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let test_all_indexers_command = SonarrCommand::TestAllIndexers;
|
||||
|
||||
let result = SonarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{sonarr_network::SonarrEvent, NetworkTrait},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "trigger_automatic_search_command_handler_tests.rs"]
|
||||
mod trigger_automatic_search_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum SonarrTriggerAutomaticSearchCommand {
|
||||
#[command(about = "Trigger an automatic search for the series with the specified ID")]
|
||||
Series {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the series you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Trigger an automatic search for the given season corresponding to the series with the given ID"
|
||||
)]
|
||||
Season {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Sonarr ID of the series whose season you wish to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
series_id: i64,
|
||||
#[arg(long, help = "The season number to search for", required = true)]
|
||||
season_number: i64,
|
||||
},
|
||||
#[command(about = "Trigger an automatic search for the episode with the specified ID")]
|
||||
Episode {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the episode you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
episode_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SonarrTriggerAutomaticSearchCommand> for Command {
|
||||
fn from(value: SonarrTriggerAutomaticSearchCommand) -> Self {
|
||||
Command::Sonarr(SonarrCommand::TriggerAutomaticSearch(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SonarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand>
|
||||
for SonarrTriggerAutomaticSearchCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: SonarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
SonarrTriggerAutomaticSearchCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
SonarrTriggerAutomaticSearchCommand::Series { series_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(series_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrTriggerAutomaticSearchCommand::Season {
|
||||
series_id,
|
||||
season_number,
|
||||
} => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(Some((series_id, season_number))).into(),
|
||||
)
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
SonarrTriggerAutomaticSearchCommand::Episode { episode_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(Some(episode_id)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
sonarr::{
|
||||
trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand, SonarrCommand,
|
||||
},
|
||||
Command,
|
||||
};
|
||||
use crate::Cli;
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_trigger_automatic_search_command_from() {
|
||||
let command = SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Command::Sonarr(SonarrCommand::TriggerAutomaticSearch(command))
|
||||
);
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_series_search_requires_series_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"trigger-automatic-search",
|
||||
"series",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_series_search_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"trigger-automatic-search",
|
||||
"series",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_season_search_requires_series_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"trigger-automatic-search",
|
||||
"season",
|
||||
"--season-number",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_season_search_requires_season_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"trigger-automatic-search",
|
||||
"season",
|
||||
"--series-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_season_search_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"trigger-automatic-search",
|
||||
"season",
|
||||
"--series-id",
|
||||
"1",
|
||||
"--season-number",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_episode_search_requires_episode_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"trigger-automatic-search",
|
||||
"episode",
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_episode_search_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"sonarr",
|
||||
"trigger-automatic-search",
|
||||
"episode",
|
||||
"--episode-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
sonarr::trigger_automatic_search_command_handler::{
|
||||
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
|
||||
},
|
||||
CliCommandHandler,
|
||||
},
|
||||
models::{sonarr_models::SonarrSerdeable, Serdeable},
|
||||
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_trigger_automatic_series_search_command() {
|
||||
let expected_series_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::TriggerAutomaticSeriesSearch(Some(expected_series_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let trigger_automatic_series_search_command =
|
||||
SonarrTriggerAutomaticSearchCommand::Series { series_id: 1 };
|
||||
|
||||
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_series_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_trigger_automatic_season_search_command() {
|
||||
let expected_series_id = 1;
|
||||
let expected_season_number = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(Some((
|
||||
expected_series_id,
|
||||
expected_season_number,
|
||||
)))
|
||||
.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let trigger_automatic_season_search_command = SonarrTriggerAutomaticSearchCommand::Season {
|
||||
series_id: 1,
|
||||
season_number: 1,
|
||||
};
|
||||
|
||||
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_season_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_trigger_automatic_episode_search_command() {
|
||||
let expected_episode_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::default()));
|
||||
let trigger_automatic_episode_search_command =
|
||||
SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 };
|
||||
|
||||
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_episode_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,9 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler};
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{
|
||||
BlocklistItem, BlocklistItemMovie, Language, Quality, QualityWrapper,
|
||||
};
|
||||
use crate::models::radarr_models::{BlocklistItem, BlocklistItemMovie};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
@@ -584,6 +583,9 @@ mod tests {
|
||||
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -716,6 +718,43 @@ mod tests {
|
||||
assert!(app.data.radarr_data.blocklist.sort.is_none());
|
||||
assert!(!app.data.radarr_data.blocklist.sort_asc);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveRadarrBlock::Blocklist,
|
||||
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
|
||||
RadarrEvent::DeleteBlocklistItem(None)
|
||||
)]
|
||||
#[case(
|
||||
ActiveRadarrBlock::Blocklist,
|
||||
ActiveRadarrBlock::BlocklistClearAllItemsPrompt,
|
||||
RadarrEvent::ClearBlocklist
|
||||
)]
|
||||
fn test_blocklist_prompt_confirm(
|
||||
#[case] base_route: ActiveRadarrBlock,
|
||||
#[case] prompt_block: ActiveRadarrBlock,
|
||||
#[case] expected_action: RadarrEvent,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
BlocklistHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&prompt_block,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(expected_action)
|
||||
);
|
||||
assert_eq!(app.get_current_route(), &base_route.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -920,6 +959,7 @@ mod tests {
|
||||
id: 3,
|
||||
source_title: "test 1".to_owned(),
|
||||
languages: vec![Language {
|
||||
id: 1,
|
||||
name: "telgu".to_owned(),
|
||||
}],
|
||||
quality: QualityWrapper {
|
||||
@@ -928,6 +968,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
custom_formats: Some(vec![Language {
|
||||
id: 2,
|
||||
name: "nikki".to_owned(),
|
||||
}]),
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
|
||||
@@ -940,6 +981,7 @@ mod tests {
|
||||
id: 2,
|
||||
source_title: "test 2".to_owned(),
|
||||
languages: vec![Language {
|
||||
id: 3,
|
||||
name: "chinese".to_owned(),
|
||||
}],
|
||||
quality: QualityWrapper {
|
||||
@@ -949,9 +991,11 @@ mod tests {
|
||||
},
|
||||
custom_formats: Some(vec![
|
||||
Language {
|
||||
id: 4,
|
||||
name: "alex".to_owned(),
|
||||
},
|
||||
Language {
|
||||
id: 5,
|
||||
name: "English".to_owned(),
|
||||
},
|
||||
]),
|
||||
@@ -965,6 +1009,7 @@ mod tests {
|
||||
id: 1,
|
||||
source_title: "test 3".to_owned(),
|
||||
languages: vec![Language {
|
||||
id: 1,
|
||||
name: "english".to_owned(),
|
||||
}],
|
||||
quality: QualityWrapper {
|
||||
@@ -973,6 +1018,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
custom_formats: Some(vec![Language {
|
||||
id: 2,
|
||||
name: "English".to_owned(),
|
||||
}]),
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
|
||||
|
||||
@@ -182,8 +182,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
if self.active_radarr_block == &ActiveRadarrBlock::Blocklist {
|
||||
match self.key {
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::Blocklist => match self.key {
|
||||
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
@@ -204,7 +204,25 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
|
||||
.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action =
|
||||
Some(RadarrEvent::DeleteBlocklistItem(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1040,6 +1040,7 @@ mod tests {
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
RadarrData, EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::test_edit_collection_key;
|
||||
|
||||
use super::*;
|
||||
@@ -1509,6 +1510,36 @@ mod tests {
|
||||
assert!(app.data.radarr_data.collections.sort.is_none());
|
||||
assert!(!app.data.radarr_data.collections.sort_asc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_collections_prompt_confirm_confirm() {
|
||||
let mut app = App::default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.collections
|
||||
.set_items(vec![Collection::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
|
||||
|
||||
CollectionsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::UpdateAllCollectionsPrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::UpdateCollections)
|
||||
);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
&ActiveRadarrBlock::Collections.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
||||
@@ -291,19 +291,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
if self.active_radarr_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.radarr_data
|
||||
.edit_collection_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.path
|
||||
)
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.radarr_data
|
||||
.edit_collection_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.path
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::EditCollectionPrompt => {
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== &ActiveRadarrBlock::EditCollectionConfirmPrompt
|
||||
&& *key == DEFAULT_KEYBINDINGS.confirm.key
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(None));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,7 +871,15 @@ mod tests {
|
||||
|
||||
mod test_handle_key_char {
|
||||
use super::*;
|
||||
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
|
||||
use crate::{
|
||||
models::{
|
||||
servarr_data::radarr::{
|
||||
modals::EditCollectionModal, radarr_data::EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||
},
|
||||
BlockSelectionState,
|
||||
},
|
||||
network::radarr_network::RadarrEvent,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_edit_collection_root_folder_path_input_backspace() {
|
||||
@@ -927,6 +935,39 @@ mod tests {
|
||||
"h"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_collection_confirm_prompt_prompt_confirmation_confirm() {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
|
||||
app.data.radarr_data.selected_block =
|
||||
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.selected_block
|
||||
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
EditCollectionHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::EditCollectionPrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
&ActiveRadarrBlock::Collections.into()
|
||||
);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::EditCollection(None))
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -385,6 +385,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +349,9 @@ mod tests {
|
||||
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -450,6 +453,47 @@ mod tests {
|
||||
);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveRadarrBlock::Downloads,
|
||||
ActiveRadarrBlock::DeleteDownloadPrompt,
|
||||
RadarrEvent::DeleteDownload(None)
|
||||
)]
|
||||
#[case(
|
||||
ActiveRadarrBlock::Downloads,
|
||||
ActiveRadarrBlock::UpdateDownloadsPrompt,
|
||||
RadarrEvent::UpdateDownloads
|
||||
)]
|
||||
fn test_downloads_prompt_confirm_submit(
|
||||
#[case] base_route: ActiveRadarrBlock,
|
||||
#[case] prompt_block: ActiveRadarrBlock,
|
||||
#[case] expected_action: RadarrEvent,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.downloads
|
||||
.set_items(vec![DownloadRecord::default()]);
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
DownloadsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&prompt_block,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(expected_action)
|
||||
);
|
||||
assert_eq!(app.get_current_route(), &base_route.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -119,8 +119,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
|
||||
match self.key {
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::Downloads => match self.key {
|
||||
_ if *key == DEFAULT_KEYBINDINGS.update.key => {
|
||||
self
|
||||
.app
|
||||
@@ -130,7 +130,24 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveRadarrBlock::DeleteDownloadPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveRadarrBlock::UpdateDownloadsPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,6 +429,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
|
||||
.tags
|
||||
);
|
||||
}
|
||||
ActiveRadarrBlock::EditIndexerPrompt => {
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== &ActiveRadarrBlock::EditIndexerConfirmPrompt
|
||||
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(None));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
@@ -14,7 +14,7 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
|
||||
use crate::models::BlockSelectionState;
|
||||
|
||||
@@ -69,7 +69,7 @@ mod tests {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
@@ -334,7 +334,7 @@ mod tests {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
};
|
||||
@@ -759,7 +759,7 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::{
|
||||
servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, BlockSelectionState,
|
||||
};
|
||||
@@ -1224,7 +1224,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
@@ -1281,7 +1281,10 @@ mod tests {
|
||||
|
||||
mod test_handle_key_char {
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
|
||||
use super::*;
|
||||
@@ -1560,6 +1563,37 @@ mod tests {
|
||||
"h"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_prompt_prompt_confirmation_confirm() {
|
||||
let mut app = App::default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into());
|
||||
app.data.radarr_data.selected_block =
|
||||
BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.selected_block
|
||||
.set_index(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
|
||||
|
||||
EditIndexerHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::EditIndexerPrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
|
||||
assert!(app.data.radarr_data.edit_indexer_modal.is_some());
|
||||
assert!(app.should_refresh);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::EditIndexer(None))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -241,19 +241,35 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
if self.active_radarr_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.radarr_data
|
||||
.indexer_settings
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.whitelisted_hardcoded_subs
|
||||
)
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.radarr_data
|
||||
.indexer_settings
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.whitelisted_hardcoded_subs
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== &ActiveRadarrBlock::IndexerSettingsConfirmPrompt
|
||||
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action =
|
||||
Some(RadarrEvent::EditAllIndexerSettings(None));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,7 +851,13 @@ mod tests {
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_str_eq;
|
||||
|
||||
use crate::models::radarr_models::IndexerSettings;
|
||||
use crate::{
|
||||
models::{
|
||||
radarr_models::IndexerSettings,
|
||||
servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, BlockSelectionState,
|
||||
},
|
||||
network::radarr_network::RadarrEvent,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -909,6 +915,37 @@ mod tests {
|
||||
"h"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() {
|
||||
let mut app = App::default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.data.radarr_data.selected_block =
|
||||
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.selected_block
|
||||
.set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
|
||||
IndexerSettingsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::EditAllIndexerSettings(None))
|
||||
);
|
||||
assert!(app.data.radarr_data.indexer_settings.is_some());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,16 +9,15 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::indexers::IndexersHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::Indexer;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::Indexer;
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::radarr_models::Indexer;
|
||||
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
|
||||
|
||||
use super::*;
|
||||
@@ -65,7 +64,6 @@ mod tests {
|
||||
}
|
||||
|
||||
mod test_handle_home_end {
|
||||
use crate::models::radarr_models::Indexer;
|
||||
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
|
||||
|
||||
use super::*;
|
||||
@@ -239,11 +237,11 @@ mod tests {
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use crate::models::radarr_models::{Indexer, IndexerField};
|
||||
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::{Indexer, IndexerField};
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::{Number, Value};
|
||||
@@ -464,7 +462,10 @@ mod tests {
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::models::servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS;
|
||||
use crate::{
|
||||
models::servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
|
||||
network::radarr_network::RadarrEvent,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -696,6 +697,33 @@ mod tests {
|
||||
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_prompt_confirm() {
|
||||
let mut app = App::default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
|
||||
|
||||
IndexersHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::DeleteIndexerPrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::DeleteIndexer(None))
|
||||
);
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
||||
@@ -10,7 +10,8 @@ use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::Scrollable;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
mod edit_indexer_handler;
|
||||
@@ -166,8 +167,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
if self.active_radarr_block == &ActiveRadarrBlock::Indexers {
|
||||
match self.key {
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::Indexers => match self.key {
|
||||
_ if *key == DEFAULT_KEYBINDINGS.add.key => {
|
||||
self
|
||||
.app
|
||||
@@ -194,7 +195,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
|
||||
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveRadarrBlock::DeleteIndexerPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
|
||||
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use strum::IntoEnumIterator;
|
||||
@@ -14,7 +14,7 @@ mod tests {
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
|
||||
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::simple_stateful_iterable_vec;
|
||||
|
||||
@@ -112,7 +112,7 @@ mod tests {
|
||||
|
||||
mod test_handle_home_end {
|
||||
use crate::extended_stateful_iterable_vec;
|
||||
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
|
||||
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
|
||||
|
||||
@@ -461,6 +461,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::AddMoviePrompt => {
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== &ActiveRadarrBlock::AddMovieConfirmPrompt
|
||||
&& *key == DEFAULT_KEYBINDINGS.confirm.key
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None));
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::library::add_movie_handler::AddMovieHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieSearchResult, MinimumAvailability, Monitor, RootFolder,
|
||||
};
|
||||
use crate::models::radarr_models::{AddMovieSearchResult, MinimumAvailability, MovieMonitor};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
@@ -142,7 +141,7 @@ mod tests {
|
||||
fn test_add_movie_select_monitor_scroll(
|
||||
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
|
||||
) {
|
||||
let monitor_vec = Vec::from_iter(Monitor::iter());
|
||||
let monitor_vec = Vec::from_iter(MovieMonitor::iter());
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
||||
app
|
||||
@@ -535,7 +534,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_add_movie_select_monitor_home_end() {
|
||||
let monitor_vec = Vec::from_iter(Monitor::iter());
|
||||
let monitor_vec = Vec::from_iter(MovieMonitor::iter());
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
||||
app
|
||||
@@ -1494,7 +1493,13 @@ mod tests {
|
||||
|
||||
mod test_handle_key_char {
|
||||
use super::*;
|
||||
use crate::models::servarr_data::radarr::modals::AddMovieModal;
|
||||
use crate::{
|
||||
models::{
|
||||
servarr_data::radarr::{modals::AddMovieModal, radarr_data::ADD_MOVIE_SELECTION_BLOCKS},
|
||||
BlockSelectionState,
|
||||
},
|
||||
network::radarr_network::RadarrEvent,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_add_movie_search_input_backspace() {
|
||||
@@ -1588,6 +1593,35 @@ mod tests {
|
||||
"h"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_movie_confirm_prompt_prompt_confirmation_confirm() {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into());
|
||||
app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.selected_block
|
||||
.set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
AddMovieHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::AddMoviePrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::AddMovie(None))
|
||||
);
|
||||
assert!(app.data.radarr_data.add_movie_modal.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
|
||||
@@ -100,5 +101,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {}
|
||||
fn handle_char_key_event(&mut self) {
|
||||
if self.active_radarr_block == &ActiveRadarrBlock::DeleteMoviePrompt
|
||||
&& self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== &ActiveRadarrBlock::DeleteMovieConfirmPrompt
|
||||
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(None));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +250,51 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use crate::{
|
||||
models::{
|
||||
servarr_data::radarr::radarr_data::DELETE_MOVIE_SELECTION_BLOCKS, BlockSelectionState,
|
||||
},
|
||||
network::radarr_network::RadarrEvent,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_delete_movie_confirm_prompt_prompt_confirm() {
|
||||
let mut app = App::default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into());
|
||||
app.data.radarr_data.delete_movie_files = true;
|
||||
app.data.radarr_data.add_list_exclusion = true;
|
||||
app.data.radarr_data.selected_block =
|
||||
BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.selected_block
|
||||
.set_index(DELETE_MOVIE_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
DeleteMovieHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::DeleteMoviePrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::DeleteMovie(None))
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert!(app.data.radarr_data.delete_movie_files);
|
||||
assert!(app.data.radarr_data.add_list_exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_movie_handler_accepts() {
|
||||
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
|
||||
|
||||
@@ -327,6 +327,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
|
||||
.tags
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::EditMoviePrompt => {
|
||||
if self.app.data.radarr_data.selected_block.get_active_block()
|
||||
== &ActiveRadarrBlock::EditMovieConfirmPrompt
|
||||
&& *key == DEFAULT_KEYBINDINGS.confirm.key
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(None));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -940,7 +940,16 @@ mod tests {
|
||||
|
||||
mod test_handle_key_char {
|
||||
use super::*;
|
||||
use crate::models::servarr_data::radarr::modals::EditMovieModal;
|
||||
use crate::{
|
||||
models::{
|
||||
servarr_data::radarr::{
|
||||
modals::EditMovieModal,
|
||||
radarr_data::{EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS},
|
||||
},
|
||||
BlockSelectionState,
|
||||
},
|
||||
network::radarr_network::RadarrEvent,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_edit_movie_path_input_backspace() {
|
||||
@@ -1051,6 +1060,36 @@ mod tests {
|
||||
"h"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_movie_confirm_prompt_prompt_confirm() {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into());
|
||||
app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.selected_block
|
||||
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
|
||||
|
||||
EditMovieHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::EditMoviePrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::EditMovie(None))
|
||||
);
|
||||
assert!(app.data.radarr_data.edit_movie_modal.is_some());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,11 +11,12 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::library::{movies_sorting_options, LibraryHandler};
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{Language, Movie};
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, DELETE_MOVIE_BLOCKS, EDIT_MOVIE_BLOCKS, LIBRARY_BLOCKS,
|
||||
MOVIE_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::test_handler_delegation;
|
||||
@@ -996,6 +997,7 @@ mod tests {
|
||||
RadarrData, EDIT_MOVIE_SELECTION_BLOCKS,
|
||||
};
|
||||
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::test_edit_movie_key;
|
||||
|
||||
use super::*;
|
||||
@@ -1452,6 +1454,33 @@ mod tests {
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
|
||||
assert!(app.data.radarr_data.movies.sort.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_movies_prompt_confirm() {
|
||||
let mut app = App::default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into());
|
||||
|
||||
LibraryHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::UpdateAllMoviesPrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::UpdateAllMovies)
|
||||
);
|
||||
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1778,6 +1807,7 @@ mod tests {
|
||||
id: 3,
|
||||
title: "test 1".into(),
|
||||
original_language: Language {
|
||||
id: 1,
|
||||
name: "English".to_owned(),
|
||||
},
|
||||
size_on_disk: 1024,
|
||||
@@ -1794,6 +1824,7 @@ mod tests {
|
||||
id: 2,
|
||||
title: "test 2".into(),
|
||||
original_language: Language {
|
||||
id: 2,
|
||||
name: "Chinese".to_owned(),
|
||||
},
|
||||
size_on_disk: 2048,
|
||||
@@ -1810,6 +1841,7 @@ mod tests {
|
||||
id: 1,
|
||||
title: "test 3".into(),
|
||||
original_language: Language {
|
||||
id: 3,
|
||||
name: "Japanese".to_owned(),
|
||||
},
|
||||
size_on_disk: 512,
|
||||
|
||||
@@ -386,6 +386,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
|
||||
self.app.data.radarr_data.movies.filter.as_mut().unwrap()
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::UpdateAllMoviesPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
|
||||
use crate::models::radarr_models::{Language, Release};
|
||||
use crate::models::radarr_models::RadarrRelease;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, EDIT_MOVIE_SELECTION_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{BlockSelectionState, Scrollable};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
@@ -47,18 +48,28 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
let movie_details_modal_is_ready =
|
||||
if let Some(movie_details_modal) = &self.app.data.radarr_data.movie_details_modal {
|
||||
!movie_details_modal.movie_details.is_empty()
|
||||
|| !movie_details_modal.movie_history.is_empty()
|
||||
|| !movie_details_modal.movie_cast.is_empty()
|
||||
|| !movie_details_modal.movie_crew.is_empty()
|
||||
|| !movie_details_modal.movie_releases.is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
!self.app.is_loading && movie_details_modal_is_ready
|
||||
if let Some(movie_details_modal) = &self.app.data.radarr_data.movie_details_modal {
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::MovieDetails => {
|
||||
!self.app.is_loading && !movie_details_modal.movie_details.is_empty()
|
||||
}
|
||||
ActiveRadarrBlock::MovieHistory => {
|
||||
!self.app.is_loading && !movie_details_modal.movie_history.is_empty()
|
||||
}
|
||||
ActiveRadarrBlock::Cast => {
|
||||
!self.app.is_loading && !movie_details_modal.movie_cast.is_empty()
|
||||
}
|
||||
ActiveRadarrBlock::Crew => {
|
||||
!self.app.is_loading && !movie_details_modal.movie_crew.is_empty()
|
||||
}
|
||||
ActiveRadarrBlock::ManualSearch => {
|
||||
!self.app.is_loading && !movie_details_modal.movie_releases.is_empty()
|
||||
}
|
||||
_ => !self.app.is_loading,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
@@ -464,12 +475,38 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveRadarrBlock::AutomaticallySearchMoviePrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action =
|
||||
Some(RadarrEvent::TriggerAutomaticSearch(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveRadarrBlock::UpdateAndScanPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveRadarrBlock::ManualSearchConfirmPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action =
|
||||
Some(RadarrEvent::DownloadRelease(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn releases_sorting_options() -> Vec<SortOption<Release>> {
|
||||
fn releases_sorting_options() -> Vec<SortOption<RadarrRelease>> {
|
||||
vec![
|
||||
SortOption {
|
||||
name: "Source",
|
||||
@@ -524,6 +561,7 @@ fn releases_sorting_options() -> Vec<SortOption<Release>> {
|
||||
name: "Language",
|
||||
cmp_fn: Some(|a, b| {
|
||||
let default_language_vec = vec![Language {
|
||||
id: 1,
|
||||
name: "_".to_owned(),
|
||||
}];
|
||||
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||
|
||||
@@ -3,6 +3,7 @@ mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::Number;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
@@ -13,11 +14,11 @@ mod tests {
|
||||
releases_sorting_options, MovieDetailsHandler,
|
||||
};
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{
|
||||
Credit, Language, MovieHistoryItem, Quality, QualityWrapper, Release,
|
||||
};
|
||||
use crate::models::radarr_models::RadarrRelease;
|
||||
use crate::models::radarr_models::{Credit, MovieHistoryItem};
|
||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
|
||||
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||
|
||||
@@ -405,7 +406,7 @@ mod tests {
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(simple_stateful_iterable_vec!(
|
||||
Release,
|
||||
RadarrRelease,
|
||||
HorizontallyScrollableText
|
||||
));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
@@ -453,7 +454,7 @@ mod tests {
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(simple_stateful_iterable_vec!(
|
||||
Release,
|
||||
RadarrRelease,
|
||||
HorizontallyScrollableText
|
||||
));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
@@ -996,7 +997,7 @@ mod tests {
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(extended_stateful_iterable_vec!(
|
||||
Release,
|
||||
RadarrRelease,
|
||||
HorizontallyScrollableText
|
||||
));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
@@ -1054,7 +1055,7 @@ mod tests {
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(extended_stateful_iterable_vec!(
|
||||
Release,
|
||||
RadarrRelease,
|
||||
HorizontallyScrollableText
|
||||
));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
@@ -1245,10 +1246,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_manual_search_submit() {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
let mut modal = MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
..MovieDetailsModal::default()
|
||||
});
|
||||
};
|
||||
modal
|
||||
.movie_releases
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into());
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
@@ -1468,6 +1473,7 @@ mod tests {
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
RadarrData, EDIT_MOVIE_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::test_edit_movie_key;
|
||||
|
||||
use super::*;
|
||||
@@ -1484,11 +1490,22 @@ mod tests {
|
||||
)]
|
||||
active_radarr_block: ActiveRadarrBlock,
|
||||
) {
|
||||
use crate::models::radarr_models::RadarrRelease;
|
||||
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
let mut modal = MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("Test".to_owned()),
|
||||
..MovieDetailsModal::default()
|
||||
});
|
||||
};
|
||||
modal
|
||||
.movie_history
|
||||
.set_items(vec![MovieHistoryItem::default()]);
|
||||
modal.movie_cast.set_items(vec![Credit::default()]);
|
||||
modal.movie_crew.set_items(vec![Credit::default()]);
|
||||
modal
|
||||
.movie_releases
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.search.key,
|
||||
@@ -1538,10 +1555,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_sort_key() {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
..MovieDetailsModal::default()
|
||||
});
|
||||
let mut modal = MovieDetailsModal::default();
|
||||
modal.movie_releases.set_items(release_vec());
|
||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.sort.key,
|
||||
@@ -1669,10 +1685,19 @@ mod tests {
|
||||
active_radarr_block: ActiveRadarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
let mut modal = MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("Test".to_owned()),
|
||||
..MovieDetailsModal::default()
|
||||
});
|
||||
};
|
||||
modal
|
||||
.movie_history
|
||||
.set_items(vec![MovieHistoryItem::default()]);
|
||||
modal.movie_cast.set_items(vec![Credit::default()]);
|
||||
modal.movie_crew.set_items(vec![Credit::default()]);
|
||||
modal
|
||||
.movie_releases
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.update.key,
|
||||
@@ -1732,10 +1757,19 @@ mod tests {
|
||||
active_radarr_block: ActiveRadarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
let mut modal = MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("Test".to_owned()),
|
||||
..MovieDetailsModal::default()
|
||||
});
|
||||
};
|
||||
modal
|
||||
.movie_history
|
||||
.set_items(vec![MovieHistoryItem::default()]);
|
||||
modal.movie_cast.set_items(vec![Credit::default()]);
|
||||
modal.movie_crew.set_items(vec![Credit::default()]);
|
||||
modal
|
||||
.movie_releases
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.refresh.key,
|
||||
@@ -1780,11 +1814,56 @@ mod tests {
|
||||
assert_eq!(app.get_current_route(), &active_radarr_block.into());
|
||||
assert!(app.is_routing);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveRadarrBlock::AutomaticallySearchMoviePrompt,
|
||||
RadarrEvent::TriggerAutomaticSearch(None)
|
||||
)]
|
||||
#[case(
|
||||
ActiveRadarrBlock::UpdateAndScanPrompt,
|
||||
RadarrEvent::UpdateAndScan(None)
|
||||
)]
|
||||
#[case(
|
||||
ActiveRadarrBlock::ManualSearchConfirmPrompt,
|
||||
RadarrEvent::DownloadRelease(None)
|
||||
)]
|
||||
fn test_movie_info_prompt_confirm(
|
||||
#[case] prompt_block: ActiveRadarrBlock,
|
||||
#[case] expected_action: RadarrEvent,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("test".to_owned()),
|
||||
..MovieDetailsModal::default()
|
||||
});
|
||||
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&prompt_block,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
&ActiveRadarrBlock::MovieDetails.into()
|
||||
);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(expected_action)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_source() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.protocol.cmp(&b.protocol);
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||
|a, b| a.protocol.cmp(&b.protocol);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1798,7 +1877,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_age() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.age.cmp(&b.age);
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = |a, b| a.age.cmp(&b.age);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1812,7 +1891,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_rejected() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.rejected.cmp(&b.rejected);
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||
|a, b| a.rejected.cmp(&b.rejected);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1826,7 +1906,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_title() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| {
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = |a, b| {
|
||||
a.title
|
||||
.text
|
||||
.to_lowercase()
|
||||
@@ -1845,7 +1925,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_indexer() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering =
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||
|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase());
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
@@ -1860,7 +1940,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_size() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.size.cmp(&b.size);
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||
|a, b| a.size.cmp(&b.size);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1874,7 +1955,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_peers() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| {
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = |a, b| {
|
||||
let default_number = Number::from(i64::MAX);
|
||||
let seeder_a = a
|
||||
.seeders
|
||||
@@ -1904,8 +1985,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_language() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| {
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = |a, b| {
|
||||
let default_language_vec = vec![Language {
|
||||
id: 1,
|
||||
name: "_".to_owned(),
|
||||
}];
|
||||
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||
@@ -1926,7 +2008,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_quality() {
|
||||
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.quality.cmp(&b.quality);
|
||||
let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering =
|
||||
|a, b| a.quality.cmp(&b.quality);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
@@ -1949,15 +2032,39 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_details_handler_is_not_ready_when_loading() {
|
||||
#[rstest]
|
||||
fn test_movie_details_handler_is_not_ready_when_loading(
|
||||
#[values(
|
||||
ActiveRadarrBlock::MovieDetails,
|
||||
ActiveRadarrBlock::MovieHistory,
|
||||
ActiveRadarrBlock::FileInfo,
|
||||
ActiveRadarrBlock::Cast,
|
||||
ActiveRadarrBlock::Crew,
|
||||
ActiveRadarrBlock::ManualSearch,
|
||||
ActiveRadarrBlock::ManualSearch
|
||||
)]
|
||||
movie_details_block: ActiveRadarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.is_loading = true;
|
||||
let mut modal = MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string("Test".to_owned()),
|
||||
..MovieDetailsModal::default()
|
||||
};
|
||||
modal
|
||||
.movie_history
|
||||
.set_items(vec![MovieHistoryItem::default()]);
|
||||
modal.movie_cast.set_items(vec![Credit::default()]);
|
||||
modal.movie_crew.set_items(vec![Credit::default()]);
|
||||
modal
|
||||
.movie_releases
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||
|
||||
let handler = MovieDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::MovieDetails,
|
||||
&movie_details_block,
|
||||
&None,
|
||||
);
|
||||
|
||||
@@ -2060,7 +2167,9 @@ mod tests {
|
||||
let mut app = App::default();
|
||||
app.is_loading = false;
|
||||
let mut modal = MovieDetailsModal::default();
|
||||
modal.movie_releases.set_items(vec![Release::default()]);
|
||||
modal
|
||||
.movie_releases
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
app.data.radarr_data.movie_details_modal = Some(modal);
|
||||
|
||||
let handler = MovieDetailsHandler::with(
|
||||
@@ -2073,8 +2182,8 @@ mod tests {
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
fn release_vec() -> Vec<Release> {
|
||||
let release_a = Release {
|
||||
fn release_vec() -> Vec<RadarrRelease> {
|
||||
let release_a = RadarrRelease {
|
||||
protocol: "Protocol A".to_owned(),
|
||||
age: 1,
|
||||
title: HorizontallyScrollableText::from("Title A"),
|
||||
@@ -2083,6 +2192,7 @@ mod tests {
|
||||
rejected: true,
|
||||
seeders: Some(Number::from(1)),
|
||||
languages: Some(vec![Language {
|
||||
id: 1,
|
||||
name: "Language A".to_owned(),
|
||||
}]),
|
||||
quality: QualityWrapper {
|
||||
@@ -2090,9 +2200,9 @@ mod tests {
|
||||
name: "Quality A".to_owned(),
|
||||
},
|
||||
},
|
||||
..Release::default()
|
||||
..RadarrRelease::default()
|
||||
};
|
||||
let release_b = Release {
|
||||
let release_b = RadarrRelease {
|
||||
protocol: "Protocol B".to_owned(),
|
||||
age: 2,
|
||||
title: HorizontallyScrollableText::from("title B"),
|
||||
@@ -2101,6 +2211,7 @@ mod tests {
|
||||
rejected: false,
|
||||
seeders: Some(Number::from(2)),
|
||||
languages: Some(vec![Language {
|
||||
id: 2,
|
||||
name: "Language B".to_owned(),
|
||||
}]),
|
||||
quality: QualityWrapper {
|
||||
@@ -2108,9 +2219,9 @@ mod tests {
|
||||
name: "Quality B".to_owned(),
|
||||
},
|
||||
},
|
||||
..Release::default()
|
||||
..RadarrRelease::default()
|
||||
};
|
||||
let release_c = Release {
|
||||
let release_c = RadarrRelease {
|
||||
protocol: "Protocol C".to_owned(),
|
||||
age: 3,
|
||||
title: HorizontallyScrollableText::from("Title C"),
|
||||
@@ -2124,13 +2235,13 @@ mod tests {
|
||||
name: "Quality C".to_owned(),
|
||||
},
|
||||
},
|
||||
..Release::default()
|
||||
..RadarrRelease::default()
|
||||
};
|
||||
|
||||
vec![release_a, release_b, release_c]
|
||||
}
|
||||
|
||||
fn sort_options() -> Vec<SortOption<Release>> {
|
||||
fn sort_options() -> Vec<SortOption<RadarrRelease>> {
|
||||
vec![SortOption {
|
||||
name: "Test 1",
|
||||
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
|
||||
|
||||
@@ -180,6 +180,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
|
||||
self.app.data.radarr_data.edit_root_folder.as_mut().unwrap()
|
||||
)
|
||||
}
|
||||
ActiveRadarrBlock::DeleteRootFolderPrompt => {
|
||||
if *key == DEFAULT_KEYBINDINGS.confirm.key {
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action =
|
||||
Some(RadarrEvent::DeleteRootFolder(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::RootFolder;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS};
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::radarr_models::RootFolder;
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
|
||||
|
||||
use super::*;
|
||||
@@ -63,7 +63,7 @@ mod tests {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::models::radarr_models::RootFolder;
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
|
||||
|
||||
use super::*;
|
||||
@@ -554,6 +554,8 @@ mod tests {
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -706,6 +708,36 @@ mod tests {
|
||||
"h"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_prompt_confirm() {
|
||||
let mut app = App::default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.root_folders
|
||||
.set_items(vec![RootFolder::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into());
|
||||
|
||||
RootFoldersHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::DeleteRootFolderPrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::DeleteRootFolder(None))
|
||||
);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
&ActiveRadarrBlock::RootFolders.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -168,5 +168,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
|
||||
{
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
|
||||
if self.active_radarr_block == &ActiveRadarrBlock::SystemTaskStartConfirmPrompt
|
||||
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
|
||||
{
|
||||
self.app.data.radarr_data.prompt_confirm = true;
|
||||
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(None));
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{QueueEvent, Task};
|
||||
use crate::models::radarr_models::RadarrTask;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::QueueEvent;
|
||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
@@ -73,7 +74,7 @@ mod tests {
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(simple_stateful_iterable_vec!(Task, String, name));
|
||||
.set_items(simple_stateful_iterable_vec!(RadarrTask, String, name));
|
||||
|
||||
SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle();
|
||||
|
||||
@@ -101,7 +102,7 @@ mod tests {
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(simple_stateful_iterable_vec!(Task, String, name));
|
||||
.set_items(simple_stateful_iterable_vec!(RadarrTask, String, name));
|
||||
|
||||
SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle();
|
||||
|
||||
@@ -317,7 +318,7 @@ mod tests {
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(extended_stateful_iterable_vec!(Task, String, name));
|
||||
.set_items(extended_stateful_iterable_vec!(RadarrTask, String, name));
|
||||
|
||||
SystemDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.end.key,
|
||||
@@ -356,7 +357,7 @@ mod tests {
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(extended_stateful_iterable_vec!(Task, String, name));
|
||||
.set_items(extended_stateful_iterable_vec!(RadarrTask, String, name));
|
||||
|
||||
SystemDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.end.key,
|
||||
@@ -788,7 +789,11 @@ mod tests {
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemTasks, &None)
|
||||
.handle();
|
||||
@@ -858,6 +863,8 @@ mod tests {
|
||||
mod test_handle_key_char {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
@@ -912,6 +919,32 @@ mod tests {
|
||||
assert_eq!(app.get_current_route(), &active_radarr_block.into());
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_tasks_start_task_prompt_confirm() {
|
||||
let mut app = App::default();
|
||||
app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into());
|
||||
|
||||
SystemDetailsHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
&ActiveRadarrBlock::SystemTaskStartConfirmPrompt,
|
||||
&None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.data.radarr_data.prompt_confirm_action,
|
||||
Some(RadarrEvent::StartTask(None))
|
||||
);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
&ActiveRadarrBlock::SystemTasks.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,10 +9,11 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::radarr_handlers::system::SystemHandler;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::radarr_models::{QueueEvent, Task};
|
||||
use crate::models::radarr_models::RadarrTask;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::QueueEvent;
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
@@ -104,7 +105,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.update.key,
|
||||
@@ -134,7 +139,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.update.key,
|
||||
@@ -159,7 +168,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.events.key,
|
||||
@@ -189,7 +202,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.events.key,
|
||||
@@ -214,7 +231,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
||||
|
||||
SystemHandler::with(
|
||||
@@ -243,7 +264,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::System.into());
|
||||
|
||||
SystemHandler::with(
|
||||
@@ -270,7 +295,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.logs.key,
|
||||
@@ -308,7 +337,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.logs.key,
|
||||
@@ -334,7 +367,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.tasks.key,
|
||||
@@ -364,7 +401,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(vec![QueueEvent::default()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.tasks.key,
|
||||
@@ -429,7 +470,11 @@ mod tests {
|
||||
fn test_system_handler_is_not_ready_when_logs_is_empty() {
|
||||
let mut app = App::default();
|
||||
app.is_loading = false;
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
@@ -472,7 +517,11 @@ mod tests {
|
||||
let mut app = App::default();
|
||||
app.is_loading = false;
|
||||
app.data.radarr_data.logs.set_items(vec!["test".into()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
|
||||
let system_handler = SystemHandler::with(
|
||||
&DEFAULT_KEYBINDINGS.update.key,
|
||||
@@ -489,7 +538,11 @@ mod tests {
|
||||
let mut app = App::default();
|
||||
app.is_loading = false;
|
||||
app.data.radarr_data.logs.set_items(vec!["test".into()]);
|
||||
app.data.radarr_data.tasks.set_items(vec![Task::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.tasks
|
||||
.set_items(vec![RadarrTask::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
|
||||
+61
-43
@@ -1,28 +1,30 @@
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
use anyhow::Result;
|
||||
use std::panic::PanicHookInfo;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::{io, panic, process};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use clap::{
|
||||
command, crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser,
|
||||
};
|
||||
use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser};
|
||||
use clap_complete::generate;
|
||||
use colored::Colorize;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, size, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use log::error;
|
||||
use log::{error, warn};
|
||||
use network::NetworkTrait;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
use reqwest::Client;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use utils::{
|
||||
build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::cli::Command;
|
||||
@@ -41,9 +43,6 @@ mod network;
|
||||
mod ui;
|
||||
mod utils;
|
||||
|
||||
static MIN_TERM_WIDTH: u16 = 205;
|
||||
static MIN_TERM_HEIGHT: u16 = 40;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = crate_name!(),
|
||||
@@ -62,6 +61,21 @@ static MIN_TERM_HEIGHT: u16 = 40;
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
env = "MANAGARR_DISABLE_SPINNER",
|
||||
help = "Disable the spinner (can sometimes make parsing output challenging)"
|
||||
)]
|
||||
disable_spinner: bool,
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
value_parser,
|
||||
env = "MANAGARR_CONFIG_FILE",
|
||||
help = "The Managarr configuration file to use"
|
||||
)]
|
||||
config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -73,7 +87,14 @@ async fn main() -> Result<()> {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
let args = Cli::parse();
|
||||
let config = confy::load("managarr", "config")?;
|
||||
let config = if let Some(ref config_file) = args.config {
|
||||
load_config(config_file.to_str().expect("Invalid config file specified"))?
|
||||
} else {
|
||||
confy::load("managarr", "config")?
|
||||
};
|
||||
let spinner_disabled = args.disable_spinner;
|
||||
config.validate();
|
||||
let reqwest_client = build_network_client(&config);
|
||||
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
|
||||
let cancellation_token = CancellationToken::new();
|
||||
let ctrlc_cancellation_token = cancellation_token.clone();
|
||||
@@ -87,29 +108,30 @@ async fn main() -> Result<()> {
|
||||
|
||||
let app = Arc::new(Mutex::new(App::new(
|
||||
sync_network_tx,
|
||||
config,
|
||||
config.clone(),
|
||||
cancellation_token.clone(),
|
||||
)));
|
||||
|
||||
match args.command {
|
||||
Some(command) => match command {
|
||||
Command::Radarr(_) => {
|
||||
let app_nw = Arc::clone(&app);
|
||||
let mut network = Network::new(&app_nw, cancellation_token);
|
||||
|
||||
if let Err(e) = cli::handle_command(&app, command, &mut network).await {
|
||||
eprintln!("error: {}", e.to_string().red());
|
||||
process::exit(1);
|
||||
Command::Radarr(_) | Command::Sonarr(_) => {
|
||||
if spinner_disabled {
|
||||
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||
} else {
|
||||
start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
||||
}
|
||||
}
|
||||
Command::Completions { shell } => {
|
||||
let mut cli = Cli::command();
|
||||
generate(shell, &mut cli, "managarr", &mut io::stdout())
|
||||
}
|
||||
Command::TailLogs { no_color } => tail_logs(no_color).await,
|
||||
},
|
||||
None => {
|
||||
let app_nw = Arc::clone(&app);
|
||||
std::thread::spawn(move || start_networking(sync_network_rx, &app_nw, cancellation_token));
|
||||
std::thread::spawn(move || {
|
||||
start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client)
|
||||
});
|
||||
start_ui(&app).await?;
|
||||
}
|
||||
}
|
||||
@@ -122,28 +144,29 @@ async fn start_networking(
|
||||
mut network_rx: Receiver<NetworkEvent>,
|
||||
app: &Arc<Mutex<App<'_>>>,
|
||||
cancellation_token: CancellationToken,
|
||||
client: Client,
|
||||
) {
|
||||
let mut network = Network::new(app, cancellation_token);
|
||||
let mut network = Network::new(app, cancellation_token, client);
|
||||
|
||||
while let Some(network_event) = network_rx.recv().await {
|
||||
if let Err(e) = network.handle_network_event(network_event).await {
|
||||
error!("Encountered an error handling network event: {e:?}");
|
||||
loop {
|
||||
select! {
|
||||
Some(network_event) = network_rx.recv() => {
|
||||
if let Err(e) = network.handle_network_event(network_event).await {
|
||||
error!("Encountered an error handling network event: {e:?}");
|
||||
}
|
||||
}
|
||||
_ = network.cancellation_token.cancelled() => {
|
||||
warn!("Clearing network channel");
|
||||
while network_rx.try_recv().is_ok() {
|
||||
// Discard the message
|
||||
}
|
||||
network.reset_cancellation_token().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> {
|
||||
let (width, height) = size()?;
|
||||
if width < MIN_TERM_WIDTH || height < MIN_TERM_HEIGHT {
|
||||
return Err(anyhow!(
|
||||
"Terminal too small. Minimum size required: {}x{}; current terminal size: {}x{}",
|
||||
MIN_TERM_WIDTH,
|
||||
MIN_TERM_HEIGHT,
|
||||
width,
|
||||
height
|
||||
));
|
||||
}
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode()?;
|
||||
|
||||
@@ -214,14 +237,9 @@ fn panic_hook(info: &PanicHookInfo<'_>) {
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn panic_hook(info: &PanicHookInfo<'_>) {
|
||||
use human_panic::{handle_dump, print_msg, Metadata};
|
||||
use human_panic::{handle_dump, metadata, print_msg};
|
||||
|
||||
let meta = Metadata {
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
name: env!("CARGO_PKG_NAME").into(),
|
||||
authors: env!("CARGO_PKG_AUTHORS").replace(":", ", ").into(),
|
||||
homepage: env!("CARGO_PKG_HOMEPAGE").into(),
|
||||
};
|
||||
let meta = metadata!();
|
||||
let file_path = handle_dump(&meta, info);
|
||||
disable_raw_mode().unwrap();
|
||||
execute!(io::stdout(), LeaveAlternateScreen).unwrap();
|
||||
|
||||
+21
-1
@@ -6,10 +6,15 @@ use radarr_models::RadarrSerdeable;
|
||||
use regex::Regex;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Number;
|
||||
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use sonarr_models::SonarrSerdeable;
|
||||
pub mod radarr_models;
|
||||
pub mod servarr_data;
|
||||
pub mod servarr_models;
|
||||
pub mod sonarr_models;
|
||||
pub mod stateful_list;
|
||||
pub mod stateful_table;
|
||||
pub mod stateful_tree;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "model_tests.rs"]
|
||||
@@ -20,7 +25,7 @@ mod model_tests;
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Route {
|
||||
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
|
||||
Sonarr,
|
||||
Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
|
||||
Readarr,
|
||||
Lidarr,
|
||||
Whisparr,
|
||||
@@ -33,6 +38,11 @@ pub enum Route {
|
||||
#[serde(untagged)]
|
||||
pub enum Serdeable {
|
||||
Radarr(RadarrSerdeable),
|
||||
Sonarr(SonarrSerdeable),
|
||||
}
|
||||
|
||||
pub trait EnumDisplayStyle<'a> {
|
||||
fn to_display_str(self) -> &'a str;
|
||||
}
|
||||
|
||||
pub trait Scrollable {
|
||||
@@ -359,6 +369,16 @@ where
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn from_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let num: Number = Deserialize::deserialize(deserializer)?;
|
||||
num.as_f64().ok_or(de::Error::custom(format!(
|
||||
"Unable to convert Number to f64: {num:?}"
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn strip_non_search_characters(input: &str) -> String {
|
||||
Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]")
|
||||
.unwrap()
|
||||
|
||||
@@ -10,6 +10,7 @@ mod tests {
|
||||
use serde::de::IntoDeserializer;
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::models::from_f64;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::{from_i64, strip_non_search_characters};
|
||||
use crate::models::{
|
||||
@@ -649,6 +650,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_f64() {
|
||||
let deserializer: F64Deserializer<ValueError> = 1f64.into_deserializer();
|
||||
|
||||
assert_eq!(from_f64(deserializer), Ok(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_horizontally_scrollable_serialize() {
|
||||
let text = HorizontallyScrollableText::from("Test");
|
||||
|
||||
+33
-194
@@ -9,7 +9,11 @@ use strum_macros::EnumIter;
|
||||
|
||||
use crate::{models::HorizontallyScrollableText, serde_enum_from};
|
||||
|
||||
use super::Serdeable;
|
||||
use super::servarr_models::{
|
||||
DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper,
|
||||
QueueEvent, RootFolder, SecurityConfig, Tag, Update,
|
||||
};
|
||||
use super::{EnumDisplayStyle, Serdeable};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_models_tests.rs"]
|
||||
@@ -25,7 +29,7 @@ pub struct AddMovieBody {
|
||||
pub minimum_availability: String,
|
||||
pub monitored: bool,
|
||||
pub tags: Vec<i64>,
|
||||
pub add_options: AddOptions,
|
||||
pub add_options: AddMovieOptions,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
@@ -47,16 +51,11 @@ pub struct AddMovieSearchResult {
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddOptions {
|
||||
pub struct AddMovieOptions {
|
||||
pub monitor: String,
|
||||
pub search_for_movie: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Debug)]
|
||||
pub struct AddRootFolderBody {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BlocklistResponse {
|
||||
pub records: Vec<BlocklistItem>,
|
||||
@@ -117,12 +116,6 @@ pub struct CollectionMovie {
|
||||
pub ratings: RatingsList,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommandBody {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Credit {
|
||||
@@ -150,15 +143,6 @@ pub struct DeleteMovieParams {
|
||||
pub add_list_exclusion: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DiskSpace {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub free_space: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub total_space: i64,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadRecord {
|
||||
@@ -195,22 +179,6 @@ pub struct EditCollectionParams {
|
||||
pub search_on_add: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditIndexerParams {
|
||||
pub indexer_id: i64,
|
||||
pub name: Option<String>,
|
||||
pub enable_rss: Option<bool>,
|
||||
pub enable_automatic_search: Option<bool>,
|
||||
pub enable_interactive_search: Option<bool>,
|
||||
pub url: Option<String>,
|
||||
pub api_key: Option<String>,
|
||||
pub seed_ratio: Option<String>,
|
||||
pub tags: Option<Vec<i64>>,
|
||||
pub priority: Option<i64>,
|
||||
pub clear_tags: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditMovieParams {
|
||||
@@ -223,35 +191,6 @@ pub struct EditMovieParams {
|
||||
pub clear_tags: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Indexer {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub name: Option<String>,
|
||||
pub implementation: Option<String>,
|
||||
pub implementation_name: Option<String>,
|
||||
pub config_contract: Option<String>,
|
||||
pub supports_rss: bool,
|
||||
pub supports_search: bool,
|
||||
pub fields: Option<Vec<IndexerField>>,
|
||||
pub enable_rss: bool,
|
||||
pub enable_automatic_search: bool,
|
||||
pub enable_interactive_search: bool,
|
||||
pub protocol: String,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub priority: i64,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub download_client_id: i64,
|
||||
pub tags: Vec<Number>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct IndexerField {
|
||||
pub name: Option<String>,
|
||||
pub value: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IndexerSettings {
|
||||
@@ -290,28 +229,6 @@ pub struct IndexerValidationFailure {
|
||||
pub severity: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||
pub struct Language {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Log {
|
||||
pub time: DateTime<Utc>,
|
||||
pub exception: Option<String>,
|
||||
pub exception_type: Option<String>,
|
||||
pub level: String,
|
||||
pub logger: Option<String>,
|
||||
pub message: Option<String>,
|
||||
pub method: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct LogResponse {
|
||||
pub records: Vec<Log>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
|
||||
#[derivative(Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -360,8 +277,8 @@ impl Display for MinimumAvailability {
|
||||
}
|
||||
}
|
||||
|
||||
impl MinimumAvailability {
|
||||
pub fn to_display_str<'a>(self) -> &'a str {
|
||||
impl<'a> EnumDisplayStyle<'a> for MinimumAvailability {
|
||||
fn to_display_str(self) -> &'a str {
|
||||
match self {
|
||||
MinimumAvailability::Tba => "TBA",
|
||||
MinimumAvailability::Announced => "Announced",
|
||||
@@ -372,30 +289,30 @@ impl MinimumAvailability {
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum)]
|
||||
pub enum Monitor {
|
||||
pub enum MovieMonitor {
|
||||
#[default]
|
||||
MovieOnly,
|
||||
MovieAndCollection,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Display for Monitor {
|
||||
impl Display for MovieMonitor {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let monitor = match self {
|
||||
Monitor::MovieOnly => "movieOnly",
|
||||
Monitor::MovieAndCollection => "movieAndCollection",
|
||||
Monitor::None => "none",
|
||||
MovieMonitor::MovieOnly => "movieOnly",
|
||||
MovieMonitor::MovieAndCollection => "movieAndCollection",
|
||||
MovieMonitor::None => "none",
|
||||
};
|
||||
write!(f, "{monitor}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Monitor {
|
||||
pub fn to_display_str<'a>(self) -> &'a str {
|
||||
impl<'a> EnumDisplayStyle<'a> for MovieMonitor {
|
||||
fn to_display_str(self) -> &'a str {
|
||||
match self {
|
||||
Monitor::MovieOnly => "Movie only",
|
||||
Monitor::MovieAndCollection => "Movie and Collection",
|
||||
Monitor::None => "None",
|
||||
MovieMonitor::MovieOnly => "Movie only",
|
||||
MovieMonitor::MovieAndCollection => "Movie and Collection",
|
||||
MovieMonitor::None => "None",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -464,45 +381,6 @@ pub struct MovieHistoryItem {
|
||||
pub event_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||
pub struct Quality {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct QualityProfile {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<(&i64, &String)> for QualityProfile {
|
||||
fn from(value: (&i64, &String)) -> Self {
|
||||
QualityProfile {
|
||||
id: *value.0,
|
||||
name: value.1.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||
pub struct QualityWrapper {
|
||||
pub quality: Quality,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueueEvent {
|
||||
pub trigger: String,
|
||||
pub name: String,
|
||||
pub command_name: String,
|
||||
pub status: String,
|
||||
pub queued: DateTime<Utc>,
|
||||
pub started: Option<DateTime<Utc>>,
|
||||
pub ended: Option<DateTime<Utc>>,
|
||||
pub duration: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[derivative(Default)]
|
||||
pub struct Rating {
|
||||
@@ -521,7 +399,7 @@ pub struct RatingsList {
|
||||
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(default)]
|
||||
pub struct Release {
|
||||
pub struct RadarrRelease {
|
||||
pub guid: String,
|
||||
pub protocol: String,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
@@ -542,24 +420,12 @@ pub struct Release {
|
||||
|
||||
#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReleaseDownloadBody {
|
||||
pub struct RadarrReleaseDownloadBody {
|
||||
pub guid: String,
|
||||
pub indexer_id: i64,
|
||||
pub movie_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RootFolder {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub path: String,
|
||||
pub accessible: bool,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub free_space: i64,
|
||||
pub unmapped_folders: Option<Vec<UnmappedFolder>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemStatus {
|
||||
@@ -567,18 +433,11 @@ pub struct SystemStatus {
|
||||
pub start_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct Tag {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Task {
|
||||
pub struct RadarrTask {
|
||||
pub name: String,
|
||||
pub task_name: TaskName,
|
||||
pub task_name: RadarrTaskName,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub interval: i64,
|
||||
pub last_execution: DateTime<Utc>,
|
||||
@@ -588,7 +447,7 @@ pub struct Task {
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub enum TaskName {
|
||||
pub enum RadarrTaskName {
|
||||
#[default]
|
||||
ApplicationCheckUpdate,
|
||||
Backup,
|
||||
@@ -603,7 +462,7 @@ pub enum TaskName {
|
||||
RssSync,
|
||||
}
|
||||
|
||||
impl Display for TaskName {
|
||||
impl Display for RadarrTaskName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let task_name = serde_json::to_string(&self)
|
||||
.expect("Unable to serialize task name")
|
||||
@@ -612,30 +471,6 @@ impl Display for TaskName {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct UnmappedFolder {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Update {
|
||||
pub version: String,
|
||||
pub release_date: DateTime<Utc>,
|
||||
pub installed: bool,
|
||||
pub latest: bool,
|
||||
pub installed_on: Option<DateTime<Utc>>,
|
||||
pub changes: UpdateChanges,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateChanges {
|
||||
pub new: Option<Vec<String>>,
|
||||
pub fixed: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -647,6 +482,7 @@ pub enum RadarrSerdeable {
|
||||
Credits(Vec<Credit>),
|
||||
DiskSpaces(Vec<DiskSpace>),
|
||||
DownloadsResponse(DownloadsResponse),
|
||||
HostConfig(HostConfig),
|
||||
Indexers(Vec<Indexer>),
|
||||
IndexerSettings(IndexerSettings),
|
||||
LogResponse(LogResponse),
|
||||
@@ -655,11 +491,12 @@ pub enum RadarrSerdeable {
|
||||
Movies(Vec<Movie>),
|
||||
QualityProfiles(Vec<QualityProfile>),
|
||||
QueueEvents(Vec<QueueEvent>),
|
||||
Releases(Vec<Release>),
|
||||
Releases(Vec<RadarrRelease>),
|
||||
RootFolders(Vec<RootFolder>),
|
||||
SecurityConfig(SecurityConfig),
|
||||
SystemStatus(SystemStatus),
|
||||
Tags(Vec<Tag>),
|
||||
Tasks(Vec<Task>),
|
||||
Tasks(Vec<RadarrTask>),
|
||||
Updates(Vec<Update>),
|
||||
AddMovieSearchResults(Vec<AddMovieSearchResult>),
|
||||
IndexerTestResults(Vec<IndexerTestResult>),
|
||||
@@ -686,6 +523,7 @@ serde_enum_from!(
|
||||
Credits(Vec<Credit>),
|
||||
DiskSpaces(Vec<DiskSpace>),
|
||||
DownloadsResponse(DownloadsResponse),
|
||||
HostConfig(HostConfig),
|
||||
Indexers(Vec<Indexer>),
|
||||
IndexerSettings(IndexerSettings),
|
||||
LogResponse(LogResponse),
|
||||
@@ -694,11 +532,12 @@ serde_enum_from!(
|
||||
Movies(Vec<Movie>),
|
||||
QualityProfiles(Vec<QualityProfile>),
|
||||
QueueEvents(Vec<QueueEvent>),
|
||||
Releases(Vec<Release>),
|
||||
Releases(Vec<RadarrRelease>),
|
||||
RootFolders(Vec<RootFolder>),
|
||||
SecurityConfig(SecurityConfig),
|
||||
SystemStatus(SystemStatus),
|
||||
Tags(Vec<Tag>),
|
||||
Tasks(Vec<Task>),
|
||||
Tasks(Vec<RadarrTask>),
|
||||
Updates(Vec<Update>),
|
||||
AddMovieSearchResults(Vec<AddMovieSearchResult>),
|
||||
IndexerTestResults(Vec<IndexerTestResult>),
|
||||
|
||||
@@ -6,17 +6,18 @@ mod tests {
|
||||
use crate::models::{
|
||||
radarr_models::{
|
||||
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, DiskSpace,
|
||||
DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult, Log,
|
||||
LogResponse, MinimumAvailability, Monitor, Movie, MovieHistoryItem, QualityProfile,
|
||||
QueueEvent, RadarrSerdeable, Release, RootFolder, SystemStatus, Tag, Task, TaskName, Update,
|
||||
DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult,
|
||||
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, QualityProfile, RadarrRelease,
|
||||
RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus, Tag, Update,
|
||||
},
|
||||
Serdeable,
|
||||
servarr_models::{HostConfig, Log, LogResponse, QueueEvent, RootFolder, SecurityConfig},
|
||||
EnumDisplayStyle, Serdeable,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_task_name_display() {
|
||||
assert_str_eq!(
|
||||
TaskName::ApplicationCheckUpdate.to_string(),
|
||||
RadarrTaskName::ApplicationCheckUpdate.to_string(),
|
||||
"ApplicationCheckUpdate"
|
||||
);
|
||||
}
|
||||
@@ -42,22 +43,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_monitor_display() {
|
||||
assert_str_eq!(Monitor::MovieOnly.to_string(), "movieOnly");
|
||||
assert_str_eq!(MovieMonitor::MovieOnly.to_string(), "movieOnly");
|
||||
assert_str_eq!(
|
||||
Monitor::MovieAndCollection.to_string(),
|
||||
MovieMonitor::MovieAndCollection.to_string(),
|
||||
"movieAndCollection"
|
||||
);
|
||||
assert_str_eq!(Monitor::None.to_string(), "none");
|
||||
assert_str_eq!(MovieMonitor::None.to_string(), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_monitor_to_display_str() {
|
||||
assert_str_eq!(Monitor::MovieOnly.to_display_str(), "Movie only");
|
||||
assert_str_eq!(MovieMonitor::MovieOnly.to_display_str(), "Movie only");
|
||||
assert_str_eq!(
|
||||
Monitor::MovieAndCollection.to_display_str(),
|
||||
MovieMonitor::MovieAndCollection.to_display_str(),
|
||||
"Movie and Collection"
|
||||
);
|
||||
assert_str_eq!(Monitor::None.to_display_str(), "None");
|
||||
assert_str_eq!(MovieMonitor::None.to_display_str(), "None");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -178,6 +179,18 @@ mod tests {
|
||||
assert_eq!(radarr_serdeable, RadarrSerdeable::DiskSpaces(disk_spaces));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_host_config() {
|
||||
let host_config = HostConfig {
|
||||
port: 1234,
|
||||
..HostConfig::default()
|
||||
};
|
||||
|
||||
let radarr_serdeable: RadarrSerdeable = host_config.clone().into();
|
||||
|
||||
assert_eq!(radarr_serdeable, RadarrSerdeable::HostConfig(host_config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_downloads_response() {
|
||||
let downloads_response = DownloadsResponse {
|
||||
@@ -304,9 +317,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_releases() {
|
||||
let releases = vec![Release {
|
||||
let releases = vec![RadarrRelease {
|
||||
size: 1,
|
||||
..Release::default()
|
||||
..RadarrRelease::default()
|
||||
}];
|
||||
|
||||
let radarr_serdeable: RadarrSerdeable = releases.clone().into();
|
||||
@@ -326,6 +339,21 @@ mod tests {
|
||||
assert_eq!(radarr_serdeable, RadarrSerdeable::RootFolders(root_folders));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_security_config() {
|
||||
let security_config = SecurityConfig {
|
||||
username: Some("Test".to_owned()),
|
||||
..SecurityConfig::default()
|
||||
};
|
||||
|
||||
let radarr_serdeable: RadarrSerdeable = security_config.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
radarr_serdeable,
|
||||
RadarrSerdeable::SecurityConfig(security_config)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_system_status() {
|
||||
let system_status = SystemStatus {
|
||||
@@ -355,9 +383,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_radarr_serdeable_from_tasks() {
|
||||
let tasks = vec![Task {
|
||||
let tasks = vec![RadarrTask {
|
||||
name: "test".to_owned(),
|
||||
..Task::default()
|
||||
..RadarrTask::default()
|
||||
}];
|
||||
|
||||
let radarr_serdeable: RadarrSerdeable = tasks.clone().into();
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod modals;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
pub struct EditIndexerModal {
|
||||
pub name: HorizontallyScrollableText,
|
||||
pub enable_rss: Option<bool>,
|
||||
pub enable_automatic_search: Option<bool>,
|
||||
pub enable_interactive_search: Option<bool>,
|
||||
pub url: HorizontallyScrollableText,
|
||||
pub api_key: HorizontallyScrollableText,
|
||||
pub seed_ratio: HorizontallyScrollableText,
|
||||
pub tags: HorizontallyScrollableText,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq, Debug)]
|
||||
pub struct IndexerTestResultModalItem {
|
||||
pub name: String,
|
||||
pub is_valid: bool,
|
||||
pub validation_failures: HorizontallyScrollableText,
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::models::radarr_models::{
|
||||
Collection, Credit, Indexer, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release,
|
||||
RootFolder,
|
||||
Collection, Credit, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrRelease,
|
||||
};
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
||||
use crate::models::servarr_models::{Indexer, RootFolder};
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||
@@ -22,19 +23,7 @@ pub struct MovieDetailsModal {
|
||||
pub movie_history: StatefulTable<MovieHistoryItem>,
|
||||
pub movie_cast: StatefulTable<Credit>,
|
||||
pub movie_crew: StatefulTable<Credit>,
|
||||
pub movie_releases: StatefulTable<Release>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
pub struct EditIndexerModal {
|
||||
pub name: HorizontallyScrollableText,
|
||||
pub enable_rss: Option<bool>,
|
||||
pub enable_automatic_search: Option<bool>,
|
||||
pub enable_interactive_search: Option<bool>,
|
||||
pub url: HorizontallyScrollableText,
|
||||
pub api_key: HorizontallyScrollableText,
|
||||
pub seed_ratio: HorizontallyScrollableText,
|
||||
pub tags: HorizontallyScrollableText,
|
||||
pub movie_releases: StatefulTable<RadarrRelease>,
|
||||
}
|
||||
|
||||
impl From<&RadarrData<'_>> for EditIndexerModal {
|
||||
@@ -195,7 +184,7 @@ impl From<&RadarrData<'_>> for EditMovieModal {
|
||||
#[derive(Default)]
|
||||
pub struct AddMovieModal {
|
||||
pub root_folder_list: StatefulList<RootFolder>,
|
||||
pub monitor_list: StatefulList<Monitor>,
|
||||
pub monitor_list: StatefulList<MovieMonitor>,
|
||||
pub minimum_availability_list: StatefulList<MinimumAvailability>,
|
||||
pub quality_profile_list: StatefulList<String>,
|
||||
pub tags: HorizontallyScrollableText,
|
||||
@@ -206,7 +195,7 @@ impl From<&RadarrData<'_>> for AddMovieModal {
|
||||
let mut add_movie_modal = AddMovieModal::default();
|
||||
add_movie_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(Monitor::iter()));
|
||||
.set_items(Vec::from_iter(MovieMonitor::iter()));
|
||||
add_movie_modal
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
@@ -291,10 +280,3 @@ impl From<&RadarrData<'_>> for EditCollectionModal {
|
||||
edit_collection_modal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq, Debug)]
|
||||
pub struct IndexerTestResultModalItem {
|
||||
pub name: String,
|
||||
pub is_valid: bool,
|
||||
pub validation_failures: HorizontallyScrollableText,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::models::radarr_models::{
|
||||
Collection, Indexer, IndexerField, MinimumAvailability, Monitor, Movie, RootFolder,
|
||||
};
|
||||
use crate::models::radarr_models::{Collection, MinimumAvailability, Movie, MovieMonitor};
|
||||
use crate::models::servarr_data::radarr::modals::{
|
||||
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
|
||||
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
||||
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
@@ -184,7 +183,7 @@ mod test {
|
||||
|
||||
assert_eq!(
|
||||
add_movie_modal.monitor_list.items,
|
||||
Vec::from_iter(Monitor::iter())
|
||||
Vec::from_iter(MovieMonitor::iter())
|
||||
);
|
||||
assert_eq!(
|
||||
add_movie_modal.minimum_availability_list.items,
|
||||
|
||||
@@ -6,13 +6,14 @@ use crate::app::radarr::radarr_context_clues::{
|
||||
SYSTEM_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DiskSpace, DownloadRecord,
|
||||
Indexer, IndexerSettings, Movie, QueueEvent, RootFolder, Task,
|
||||
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord,
|
||||
IndexerSettings, Movie, RadarrTask,
|
||||
};
|
||||
use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem};
|
||||
use crate::models::servarr_data::radarr::modals::{
|
||||
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
|
||||
MovieDetailsModal,
|
||||
AddMovieModal, EditCollectionModal, EditMovieModal, MovieDetailsModal,
|
||||
};
|
||||
use crate::models::servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder};
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{
|
||||
@@ -47,7 +48,7 @@ pub struct RadarrData<'a> {
|
||||
pub collection_movies: StatefulTable<CollectionMovie>,
|
||||
pub logs: StatefulList<HorizontallyScrollableText>,
|
||||
pub log_details: StatefulList<HorizontallyScrollableText>,
|
||||
pub tasks: StatefulTable<Task>,
|
||||
pub tasks: StatefulTable<RadarrTask>,
|
||||
pub queued_events: StatefulTable<QueueEvent>,
|
||||
pub updates: ScrollableText,
|
||||
pub main_tabs: TabState,
|
||||
|
||||
@@ -19,6 +19,14 @@ mod tests {
|
||||
use crate::assert_movie_info_tabs_reset;
|
||||
use crate::models::BlockSelectionState;
|
||||
|
||||
#[test]
|
||||
fn test_from_active_radarr_block_to_route() {
|
||||
assert_eq!(
|
||||
Route::from(ActiveRadarrBlock::AddMoviePrompt),
|
||||
Route::Radarr(ActiveRadarrBlock::AddMoviePrompt, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_tuple_to_route_with_context() {
|
||||
assert_eq!(
|
||||
@@ -60,7 +68,7 @@ mod tests {
|
||||
assert_eq!(radarr_data.disk_space_vec, Vec::new());
|
||||
assert!(radarr_data.version.is_empty());
|
||||
assert_eq!(radarr_data.start_time, <DateTime<Utc>>::default());
|
||||
assert!(radarr_data.movies.items.is_empty());
|
||||
assert!(radarr_data.movies.is_empty());
|
||||
assert_eq!(radarr_data.selected_block, BlockSelectionState::default());
|
||||
assert!(radarr_data.downloads.items.is_empty());
|
||||
assert!(radarr_data.indexers.items.is_empty());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(test)]
|
||||
pub mod utils {
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieSearchResult, CollectionMovie, Credit, MovieHistoryItem, Release,
|
||||
AddMovieSearchResult, CollectionMovie, Credit, MovieHistoryItem, RadarrRelease,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
||||
@@ -24,7 +24,7 @@ pub mod utils {
|
||||
.set_items(vec![Credit::default()]);
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(vec![Release::default()]);
|
||||
.set_items(vec![RadarrRelease::default()]);
|
||||
|
||||
let mut radarr_data = RadarrData {
|
||||
delete_movie_files: true,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod modals;
|
||||
pub mod sonarr_data;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user