Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003f80098a
|
||
|
|
ad8a6743ab
|
||
|
|
c61404dc71
|
||
|
|
a5b95c165a
|
||
|
|
8ec4ec018b
|
||
|
|
642a5c0a87
|
||
|
|
9128e68d0a
|
||
|
|
ddc18ae21a
|
||
|
|
4b8b9cde57
|
||
|
|
238348f6ec
|
||
|
|
37f697d1b4
|
||
|
|
a70019b88f
|
||
|
|
fa02b70bf0 | ||
|
|
76f9f4da9a | ||
|
|
49be500b8c | ||
|
|
78918b6d88 | ||
|
|
9b98e53b55 | ||
|
|
b799fe3865 | ||
|
|
0e869e1e98 | ||
|
|
196bc55f35 | ||
|
|
e29bd67a3e | ||
|
|
424ed62229
|
||
|
|
ab7542651b
|
||
|
|
a222f40616
|
||
|
|
699e929ed2 | ||
|
|
5279d37ea2 | ||
|
|
a4b8a23424
|
||
|
|
f2ee1e71f6
|
||
|
|
bdeaf6ae35
|
||
|
|
35ed3f359d
|
||
|
|
040935178a
|
||
|
|
4f78a5940f
|
||
|
|
9822597b56
|
||
|
|
c42a46c7ca
|
||
|
|
1d936c5cb5
|
||
|
|
7b6d595397
|
||
|
|
25da032f02
|
||
|
|
e9c323c4e8
|
||
|
|
289abbd8eb
|
||
|
|
a68de41025
|
||
|
|
9d0973352f
|
||
|
|
76c490f45b
|
||
|
|
9b4b3003b5
|
||
|
|
d678f5777a
|
No files matched your search
@@ -1,11 +0,0 @@
|
|||||||
### AI assistance (if any):
|
|
||||||
- List tools here and files touched by them
|
|
||||||
|
|
||||||
### Authorship & Understanding
|
|
||||||
|
|
||||||
- [ ] I wrote or heavily modified this code myself
|
|
||||||
- [ ] I understand how it works end-to-end
|
|
||||||
- [ ] I can maintain this code in the future
|
|
||||||
- [ ] No undisclosed AI-generated code was used
|
|
||||||
- [ ] If AI assistance was used, it is documented below
|
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
### Description
|
||||||
|
<!-- What problem is this PR solving? -->
|
||||||
|
|
||||||
|
### Alternatives Considered (Optional)
|
||||||
|
<!-- What alternatives did you consider? Why did you choose this approach? -->
|
||||||
|
|
||||||
|
### AI assistance (if any)
|
||||||
|
<!-- Required if AI assistance was used to write or modify code in this PR. -->
|
||||||
|
<!-- - List tools here -->
|
||||||
|
|
||||||
|
### Authorship & Understanding
|
||||||
|
|
||||||
|
- [ ] I wrote or heavily modified this code myself. If I didn't, I gave proper attribution to AI assistance above.
|
||||||
|
- [ ] I understand how it works end-to-end
|
||||||
|
- [ ] I can maintain this code in the future
|
||||||
|
- [ ] No undisclosed AI-generated code was used
|
||||||
|
- [ ] If AI assistance was used, it is documented above
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
toolchain: [stable, beta]
|
toolchain: [stable, beta]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
- name: Run clippy action
|
- name: Run clippy action
|
||||||
uses: clechasseur/rs-clippy-check@v3
|
uses: clechasseur/rs-clippy-check@v5
|
||||||
|
|
||||||
- name: Cache Cargo dependencies
|
- name: Cache Cargo dependencies
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
name: nightly / doc
|
name: nightly / doc
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Rust nightly
|
- name: Install Rust nightly
|
||||||
uses: dtolnay/rust-toolchain@nightly
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
name: 1.95.0 / check
|
name: 1.95.0 / check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install 1.95.0
|
- name: Install 1.95.0
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
|||||||
@@ -32,19 +32,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
|
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
if: env.ACT == 'true'
|
if: env.ACT == 'true'
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
|
|
||||||
@@ -219,13 +219,13 @@ jobs:
|
|||||||
git push origin --follow-tags
|
git push origin --follow-tags
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
- name: Upload the changed Cargo files (Act)
|
- name: Upload the changed Cargo files (Act)
|
||||||
if: env.ACT == 'true'
|
if: env.ACT == 'true'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: bumped-cargo-files
|
name: bumped-cargo-files
|
||||||
path: |
|
path: |
|
||||||
@@ -267,7 +267,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -279,18 +279,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Get bumped Cargo files (Act)
|
- name: Get bumped Cargo files (Act)
|
||||||
if: env.ACT == 'true'
|
if: env.ACT == 'true'
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bumped-cargo-files
|
name: bumped-cargo-files
|
||||||
path: ${{ github.workspace }}
|
path: ${{ github.workspace }}
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v5
|
||||||
name: Cache Cargo registry
|
name: Cache Cargo registry
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v5
|
||||||
if: startsWith(matrix.job.name, 'linux-')
|
if: startsWith(matrix.job.name, 'linux-')
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/bin
|
path: ~/.cargo/bin
|
||||||
@@ -375,7 +375,7 @@ jobs:
|
|||||||
cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.sha256 artifacts/
|
cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.sha256 artifacts/
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: artifacts-${{ env.RELEASE_NAME }}
|
name: artifacts-${{ env.RELEASE_NAME }}
|
||||||
path: artifacts
|
path: artifacts
|
||||||
@@ -393,12 +393,12 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -421,7 +421,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create a GitHub Release
|
- name: Create a GitHub Release
|
||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v3
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -457,7 +457,7 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -474,12 +474,12 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Get release artifacts
|
- name: Get release artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -528,12 +528,12 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Get release artifacts
|
- name: Get release artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -592,12 +592,12 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Get release artifacts
|
- name: Get release artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -618,20 +618,20 @@ jobs:
|
|||||||
echo "Release version: ${{ env.version }}"
|
echo "Release version: ${{ env.version }}"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Push to Docker Hub
|
- name: Push to Docker Hub
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
@@ -651,13 +651,13 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Get bumped Cargo files (Act)
|
- name: Get bumped Cargo files (Act)
|
||||||
if: env.ACT == 'true'
|
if: env.ACT == 'true'
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bumped-cargo-files
|
name: bumped-cargo-files
|
||||||
path: ${{ github.workspace }}
|
path: ${{ github.workspace }}
|
||||||
@@ -668,13 +668,13 @@ jobs:
|
|||||||
git fetch --all
|
git fetch --all
|
||||||
git pull
|
git pull
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v5
|
||||||
name: Cache Cargo registry
|
name: Cache Cargo registry
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/bin
|
path: ~/.cargo/bin
|
||||||
key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }}
|
key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
# toolchain
|
# toolchain
|
||||||
toolchain: [stable, beta]
|
toolchain: [stable, beta]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install ${{ matrix.toolchain }}
|
- name: Install ${{ matrix.toolchain }}
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
# runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
# name: ubuntu / stable / minimal-versions
|
# name: ubuntu / stable / minimal-versions
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions/checkout@v4
|
# - uses: actions/checkout@v5
|
||||||
|
|
||||||
# - name: Install Rust stable
|
# - name: Install Rust stable
|
||||||
# uses: dtolnay/rust-toolchain@stable
|
# uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
# - run: vcpkg install openssl:x64-windows-static-md
|
# - run: vcpkg install openssl:x64-windows-static-md
|
||||||
# if: runner.os == 'Windows'
|
# if: runner.os == 'Windows'
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -151,7 +151,7 @@ jobs:
|
|||||||
name: ubuntu / stable / coverage
|
name: ubuntu / stable / coverage
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -176,7 +176,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload to codecov.io
|
- name: Upload to codecov.io
|
||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v7
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ act -W .github/workflows/release.yml --input_type bump=minor
|
|||||||
|
|
||||||
## Authorship Policy
|
## Authorship Policy
|
||||||
|
|
||||||
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
|
All code in this repository is written and reviewed by humans, or AI-generated where explicitly disclosed. AI-generated code (e.g., Copilot, ChatGPT,
|
||||||
Claude, etc.) is not permitted unless explicitly disclosed and approved.
|
Claude, etc.) is permitted only when explicitly disclosed and approved. For example: disclosing the use of AI-generated code in a PR description.
|
||||||
|
|
||||||
Submissions must certify that the contributor understands and can maintain the code they submit.
|
Submissions must certify that the contributor understands and can maintain the code they submit.
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
|
|||||||
- [ ]  [Bazarr](https://www.bazarr.media/)
|
- [ ]  [Bazarr](https://www.bazarr.media/)
|
||||||
- [ ]  [Tautulli](https://tautulli.com/)
|
- [ ]  [Tautulli](https://tautulli.com/)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The official Readarr repository has been archived, but Managarr's Readarr support is compatible with any Readarr forks
|
||||||
|
> that maintain compatibility with the official Readarr v1 API. See [Tested Readarr Forks](#tested-readarr-forks) below
|
||||||
|
> for forks that have been explicitly tested with Managarr, and any oddities in configuring them to work with Managarr.
|
||||||
|
|
||||||
## Try Out the Demo
|
## Try Out the Demo
|
||||||
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
|
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
|
||||||
Simply run the following command to start a demo:
|
Simply run the following command to start a demo:
|
||||||
@@ -134,8 +139,8 @@ Key:
|
|||||||
|
|
||||||
| Symbol | Status |
|
| Symbol | Status |
|
||||||
|--------|-----------|
|
|--------|-----------|
|
||||||
| ✅ | Supported |
|
| ✅ | Supported |
|
||||||
| ❌ | Missing |
|
| ❌ | Missing |
|
||||||
| 🕒 | Planned |
|
| 🕒 | Planned |
|
||||||
| 🚫 | Won't Add |
|
| 🚫 | Won't Add |
|
||||||
|
|
||||||
@@ -143,66 +148,82 @@ Key:
|
|||||||
|
|
||||||
| TUI | CLI | Feature |
|
| TUI | CLI | Feature |
|
||||||
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||||
| ✅ | ✅ | View your library, downloads, collections, and blocklist |
|
| ✅ | ✅ | 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 a specific movie including description, history, downloaded file info, or the credits |
|
||||||
| ✅ | ✅ | View details of any collection and the movies in them |
|
| ✅ | ✅ | 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 |
|
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||||
| ✅ | ✅ | Search your library or collections |
|
| ✅ | ✅ | Search your library or collections |
|
||||||
| ✅ | ✅ | Add movies to your library |
|
| ✅ | ✅ | Add movies to your library |
|
||||||
| ✅ | ✅ | Delete movies, downloads, and indexers |
|
| ✅ | ✅ | Delete movies, downloads, and indexers |
|
||||||
| ✅ | ✅ | Trigger automatic searches for movies |
|
| ✅ | ✅ | Trigger automatic searches for movies |
|
||||||
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
|
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
|
||||||
| ✅ | ✅ | Manually search for movies |
|
| ✅ | ✅ | Manually search for movies |
|
||||||
| ✅ | ✅ | Edit your movies, collections, and indexers |
|
| ✅ | ✅ | Edit your movies, collections, and indexers |
|
||||||
| ✅ | ✅ | Manage your tags |
|
| ✅ | ✅ | Manage your tags |
|
||||||
| ✅ | ✅ | Manage your root folders |
|
| ✅ | ✅ | Manage your root folders |
|
||||||
| ✅ | ✅ | Manage your blocklist |
|
| ✅ | ✅ | Manage your blocklist |
|
||||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||||
|
|
||||||
### Sonarr
|
### Sonarr
|
||||||
|
|
||||||
| TUI | CLI | Feature |
|
| TUI | CLI | Feature |
|
||||||
|-----|-----|--------------------------------------------------------------------------------------------------------------------|
|
|-----|-----|--------------------------------------------------------------------------------------------------------------------|
|
||||||
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
|
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
|
||||||
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
|
| ✅ | ✅ | 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 |
|
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||||
| ✅ | ✅ | Search your library |
|
| ✅ | ✅ | Search your library |
|
||||||
| ✅ | ✅ | Add series to your library |
|
| ✅ | ✅ | Add series to your library |
|
||||||
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
|
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
|
||||||
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
|
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
|
||||||
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
|
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
|
||||||
| ✅ | ✅ | Manually search for series, seasons, or episodes |
|
| ✅ | ✅ | Manually search for series, seasons, or episodes |
|
||||||
| ✅ | ✅ | Edit your series and indexers |
|
| ✅ | ✅ | Edit your series and indexers |
|
||||||
| ✅ | ✅ | Manage your tags |
|
| ✅ | ✅ | Manage your tags |
|
||||||
| ✅ | ✅ | Manage your root folders |
|
| ✅ | ✅ | Manage your root folders |
|
||||||
| ✅ | ✅ | Manage your blocklist |
|
| ✅ | ✅ | Manage your blocklist |
|
||||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||||
|
|
||||||
### Lidarr
|
### Lidarr
|
||||||
|
|
||||||
| TUI | CLI | Feature |
|
| TUI | CLI | Feature |
|
||||||
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||||
| ✅ | ✅ | View your library, downloads, blocklist, tracks |
|
| ✅ | ✅ | View your library, downloads, blocklist, tracks |
|
||||||
| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info |
|
| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info |
|
||||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||||
| ✅ | ✅ | Search your library |
|
| ✅ | ✅ | Search your library |
|
||||||
| ✅ | ✅ | Add artists to your library |
|
| ✅ | ✅ | Add artists to your library |
|
||||||
| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files |
|
| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files |
|
||||||
| ✅ | ✅ | Trigger automatic searches for artists or albums |
|
| ✅ | ✅ | Trigger automatic searches for artists or albums |
|
||||||
| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads |
|
| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads |
|
||||||
| ✅ | ✅ | Manually search for full artist discographies or albums |
|
| ✅ | ✅ | Manually search for full artist discographies or albums |
|
||||||
| ✅ | ✅ | Edit your artists and indexers |
|
| ✅ | ✅ | Edit your artists and indexers |
|
||||||
| ✅ | ✅ | Manage your tags |
|
| ✅ | ✅ | Manage your tags |
|
||||||
| ✅ | ✅ | Manage your root folders |
|
| ✅ | ✅ | Manage your root folders |
|
||||||
| ✅ | ✅ | Manage your blocklist |
|
| ✅ | ✅ | Manage your blocklist |
|
||||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||||
|
|
||||||
### Readarr
|
### Readarr
|
||||||
|
|
||||||
- [ ] Support for Readarr
|
| TUI | CLI | Feature |
|
||||||
|
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||||
|
| ✅ | ✅ | View your library, downloads, blocklist, editions |
|
||||||
|
| ✅ | ✅ | View details of a specific author, book, or edition including description, history, downloaded file info |
|
||||||
|
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||||
|
| ✅ | ✅ | Search your library |
|
||||||
|
| ✅ | ✅ | Add authors to your library |
|
||||||
|
| ✅ | ✅ | Delete authors, books, downloads, indexers, root folders, and book files |
|
||||||
|
| ✅ | ✅ | Trigger automatic searches for authors or books |
|
||||||
|
| ✅ | ✅ | Trigger refresh and disk scan for authors and downloads |
|
||||||
|
| ✅ | ✅ | Manually search for author releases or books |
|
||||||
|
| ✅ | ✅ | Edit your authors and indexers |
|
||||||
|
| ✅ | ✅ | Manage your tags |
|
||||||
|
| ✅ | ✅ | Manage your root folders |
|
||||||
|
| ✅ | ✅ | Manage your blocklist |
|
||||||
|
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||||
|
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||||
|
|
||||||
### Whisparr
|
### Whisparr
|
||||||
|
|
||||||
@@ -245,7 +266,7 @@ To see all available commands, simply run `managarr --help`:
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ managarr --help
|
$ managarr --help
|
||||||
managarr 0.7.0
|
managarr 0.8.0
|
||||||
Alex Clarke <alex.j.tusa@gmail.com>
|
Alex Clarke <alex.j.tusa@gmail.com>
|
||||||
|
|
||||||
A TUI and CLI to manage your Servarrs
|
A TUI and CLI to manage your Servarrs
|
||||||
@@ -443,6 +464,11 @@ lidarr:
|
|||||||
custom_headers: # Example of adding custom headers to all requests to the Servarr instance
|
custom_headers: # Example of adding custom headers to all requests to the Servarr instance
|
||||||
traefik-auth-bypass-key: someBypassKey1234567890
|
traefik-auth-bypass-key: someBypassKey1234567890
|
||||||
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
|
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
|
||||||
|
|
||||||
|
readarr:
|
||||||
|
- name: Chaptarr (Ebooks) # Example of how to use the Readarr support with a Readarr fork like Chaptarr
|
||||||
|
uri: http://192.168.0.87:8789/readarr/gr/ebook
|
||||||
|
api_token: someApiToken1234567890
|
||||||
```
|
```
|
||||||
|
|
||||||
### SSL Configuration
|
### SSL Configuration
|
||||||
@@ -552,6 +578,46 @@ Managarr supports using environment variables on startup so you don't have to al
|
|||||||
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
|
| `MANAGARR_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` |
|
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
|
||||||
|
|
||||||
|
## Tested Readarr Forks
|
||||||
|
|
||||||
|
The following Readarr forks have been explicitly tested with Managarr:
|
||||||
|
|
||||||
|
* [Bookshelf](https://github.com/pennydreadful/bookshelf)
|
||||||
|
* [Readarr-Resurrected](https://github.com/ricetim/readarr-rresurrected)
|
||||||
|
* [Librarr](https://github.com/Rorqualx/Librarr)
|
||||||
|
* [Chaptarr](https://github.com/Chaptarr/Chaptarr)
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Chaptarr houses a dedicated Readarr facade middleware that activates on a path prefix, meaning a bare config doesn't
|
||||||
|
> work. Instead, users need to configure both Audiobooks and Ebooks separately, and based on their desired metadata
|
||||||
|
> fetcher (i.e. Goodreads and Hardcover IDs). Because of this, Chaptarr-based configurations must use the `uri` key
|
||||||
|
> rather than the `host` and `port` keys. The below configuration file demonstrates all the supported variations for
|
||||||
|
> connecting Managarr to Chaptarr.
|
||||||
|
|
||||||
|
### Chaptarr Example Configuration
|
||||||
|
```yaml
|
||||||
|
readarr:
|
||||||
|
# Because Chaptarr supports both eBooks and audiobooks, the same instance must be provisioned twice if you're using
|
||||||
|
# both so Managarr can differentiate between the two. Thus, for Chaptarr, you need an eBook server configured and an
|
||||||
|
# audiobook server configured like below.
|
||||||
|
|
||||||
|
- name: eBooks (Goodreads) # Readarr originally used Goodreads as its metadata provider
|
||||||
|
uri: http://192.168.0.87:8789/readarr/gr/ebook # Note the `gr` in the path prefix indicating "GoodReads"
|
||||||
|
api_token: someApiToken1234567890
|
||||||
|
|
||||||
|
- name: Chaptarr Audiobooks (Goodreads)
|
||||||
|
uri: http://192.168.0.105:8789/readarr/gr/audiobook
|
||||||
|
api_token: someApiToken1234567890
|
||||||
|
|
||||||
|
- name: Chaptarr eBooks (Hardcover)
|
||||||
|
uri: http://192.168.0.105:8789/readarr/hc/ebook # Note the `hc` in the path prefix indicating "HardCover"
|
||||||
|
api_token: someApiToken1234567890
|
||||||
|
|
||||||
|
- name: Chaptarr Audiobooks (Hardcover)
|
||||||
|
uri: http://192.168.0.105:8789/readarr/hc/audiobook
|
||||||
|
api_token: someApiToken1234567890
|
||||||
|
```
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### Radarr
|
### Radarr
|
||||||
@@ -574,6 +640,13 @@ Managarr supports using environment variables on startup so you don't have to al
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
### Readarr
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

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

|

|
||||||

|

|
||||||
|
|||||||
@@ -5,3 +5,4 @@
|
|||||||
# It is recommended to check this file in to source control so that
|
# It is recommended to check this file in to source control so that
|
||||||
# everyone who runs the test benefits from these saved cases.
|
# everyone who runs the test benefits from these saved cases.
|
||||||
cc 56330c025ad79db641d0eb9f429ab74e95822e1fb015b58f0e158ea674cd42a1 # shrinks to list_size = 1, page_ops = 1
|
cc 56330c025ad79db641d0eb9f429ab74e95822e1fb015b58f0e158ea674cd42a1 # shrinks to list_size = 1, page_ops = 1
|
||||||
|
cc 29df5ff471eb388b0ffde573a959a539e4cf32acd9e1cea5187bba2268dd57f7 # shrinks to list_size = 1, filter_term = "𑛀"
|
||||||
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -16,6 +16,7 @@ mod tests {
|
|||||||
use crate::models::{HorizontallyScrollableText, TabRoute};
|
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||||
use crate::network::NetworkEvent;
|
use crate::network::NetworkEvent;
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -165,6 +166,36 @@ mod tests {
|
|||||||
assert!(app.is_routing);
|
assert!(app.is_routing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_named_tab_uses_selected_servarr_route_and_network_events() {
|
||||||
|
let (network_tx, mut network_rx) = mpsc::channel(500);
|
||||||
|
let mut app = App::new(
|
||||||
|
network_tx,
|
||||||
|
AppConfig {
|
||||||
|
radarr: Some(vec![ServarrConfig {
|
||||||
|
name: Some("Movies".to_owned()),
|
||||||
|
..ServarrConfig::default()
|
||||||
|
}]),
|
||||||
|
sonarr: Some(vec![ServarrConfig {
|
||||||
|
name: Some("Shows".to_owned()),
|
||||||
|
..ServarrConfig::default()
|
||||||
|
}]),
|
||||||
|
..AppConfig::default()
|
||||||
|
},
|
||||||
|
CancellationToken::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(app.server_tabs.select_tab_by_title("Shows"));
|
||||||
|
assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into());
|
||||||
|
|
||||||
|
app.on_tick().await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
network_rx.recv().await.unwrap(),
|
||||||
|
SonarrEvent::GetQualityProfiles.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_reset_cancellation_token() {
|
fn test_reset_cancellation_token() {
|
||||||
let mut app = App {
|
let mut app = App {
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ mod tests {
|
|||||||
assert!(app.is_loading);
|
assert!(app.is_loading);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rx.recv().await.unwrap(),
|
rx.recv().await.unwrap(),
|
||||||
LidarrEvent::GetTrackDetails(1).into()
|
LidarrEvent::GetTrackDetails(5).into()
|
||||||
);
|
);
|
||||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||||
assert_eq!(app.tick_count, 0);
|
assert_eq!(app.tick_count, 0);
|
||||||
@@ -518,7 +518,7 @@ mod tests {
|
|||||||
assert!(app.is_loading);
|
assert!(app.is_loading);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rx.recv().await.unwrap(),
|
rx.recv().await.unwrap(),
|
||||||
LidarrEvent::GetTrackHistory(1, 1, 1).into()
|
LidarrEvent::GetTrackHistory(1, 1, 5).into()
|
||||||
);
|
);
|
||||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||||
assert_eq!(app.tick_count, 0);
|
assert_eq!(app.tick_count, 0);
|
||||||
@@ -759,7 +759,7 @@ mod tests {
|
|||||||
album_details_modal.tracks.set_items(vec![track()]);
|
album_details_modal.tracks.set_items(vec![track()]);
|
||||||
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
|
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
|
||||||
|
|
||||||
assert_eq!(app.extract_track_id().await, 1);
|
assert_eq!(app.extract_track_id().await, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -263,14 +263,11 @@ impl App<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_route(&self) -> Route {
|
pub fn get_current_route(&self) -> Route {
|
||||||
*self.navigation_stack.last().unwrap_or(
|
self
|
||||||
&self
|
.navigation_stack
|
||||||
.server_tabs
|
.last()
|
||||||
.tabs
|
.copied()
|
||||||
.first()
|
.unwrap_or_else(|| self.server_tabs.get_active_route())
|
||||||
.expect("At least one server tab must exist")
|
|
||||||
.route,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ mod tests {
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::cli::lidarr::LidarrCommand;
|
use crate::cli::lidarr::LidarrCommand;
|
||||||
|
use crate::cli::readarr::ReadarrCommand;
|
||||||
use crate::network::lidarr_network::LidarrEvent;
|
use crate::network::lidarr_network::LidarrEvent;
|
||||||
|
use crate::network::readarr_network::ReadarrEvent;
|
||||||
use crate::{
|
use crate::{
|
||||||
Cli,
|
Cli,
|
||||||
app::App,
|
app::App,
|
||||||
@@ -24,6 +26,10 @@ mod tests {
|
|||||||
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
|
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
|
||||||
RadarrSerdeable,
|
RadarrSerdeable,
|
||||||
},
|
},
|
||||||
|
readarr_models::{
|
||||||
|
BlocklistItem as ReadarrBlocklistItem, BlocklistResponse as ReadarrBlocklistResponse,
|
||||||
|
ReadarrSerdeable,
|
||||||
|
},
|
||||||
sonarr_models::{
|
sonarr_models::{
|
||||||
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
|
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
|
||||||
SonarrSerdeable,
|
SonarrSerdeable,
|
||||||
@@ -68,6 +74,13 @@ mod tests {
|
|||||||
assert_ok!(&result);
|
assert_ok!(&result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readarr_subcommand_delegates_to_readarr() {
|
||||||
|
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "authors"]);
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_completions_requires_argument() {
|
fn test_completions_requires_argument() {
|
||||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||||
@@ -218,4 +231,35 @@ mod tests {
|
|||||||
|
|
||||||
assert_ok!(&result);
|
assert_ok!(&result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cli_handler_delegates_readarr_commands_to_the_readarr_cli_handler() {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::BlocklistResponse(
|
||||||
|
ReadarrBlocklistResponse {
|
||||||
|
records: vec![ReadarrBlocklistItem::default()],
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(ReadarrEvent::ClearBlocklist.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let clear_blocklist_command = ReadarrCommand::ClearBlocklist.into();
|
||||||
|
|
||||||
|
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -631,7 +631,7 @@ mod tests {
|
|||||||
async fn test_handle_list_track_history_command() {
|
async fn test_handle_list_track_history_command() {
|
||||||
let expected_artist_id = 1;
|
let expected_artist_id = 1;
|
||||||
let expected_album_id = 1;
|
let expected_album_id = 1;
|
||||||
let expected_track_id = 1;
|
let expected_track_id = 5;
|
||||||
let mut mock_network = MockNetworkTrait::new();
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
mock_network
|
mock_network
|
||||||
.expect_handle_network_event()
|
.expect_handle_network_event()
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrAddCommand> for ReadarrAddCommandH
|
|||||||
quality_profile_id,
|
quality_profile_id,
|
||||||
metadata_profile_id,
|
metadata_profile_id,
|
||||||
tags,
|
tags,
|
||||||
|
images: Vec::new(),
|
||||||
tag_input_string: None,
|
tag_input_string: None,
|
||||||
add_options: AddAuthorOptions {
|
add_options: AddAuthorOptions {
|
||||||
monitor,
|
monitor,
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ mod tests {
|
|||||||
quality_profile_id: 1,
|
quality_profile_id: 1,
|
||||||
metadata_profile_id: 1,
|
metadata_profile_id: 1,
|
||||||
tags: vec![1, 2],
|
tags: vec![1, 2],
|
||||||
|
images: Vec::new(),
|
||||||
tag_input_string: None,
|
tag_input_string: None,
|
||||||
add_options: AddAuthorOptions {
|
add_options: AddAuthorOptions {
|
||||||
monitor: MonitorType::All,
|
monitor: MonitorType::All,
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::cli::{
|
||||||
|
Command,
|
||||||
|
readarr::{ReadarrCommand, list_command_handler::ReadarrListCommand},
|
||||||
|
};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readarr_command_from() {
|
||||||
|
let command = ReadarrCommand::List(ReadarrListCommand::Authors);
|
||||||
|
|
||||||
|
let result = Command::from(command.clone());
|
||||||
|
|
||||||
|
assert_eq!(result, Command::Readarr(command));
|
||||||
|
}
|
||||||
|
|
||||||
mod cli {
|
mod cli {
|
||||||
use clap::CommandFactory;
|
use clap::CommandFactory;
|
||||||
use clap::error::ErrorKind;
|
use clap::error::ErrorKind;
|
||||||
@@ -243,20 +258,246 @@ mod tests {
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::cli::readarr::add_command_handler::ReadarrAddCommand;
|
||||||
|
use crate::cli::readarr::edit_command_handler::ReadarrEditCommand;
|
||||||
|
use crate::cli::readarr::get_command_handler::ReadarrGetCommand;
|
||||||
|
use crate::cli::readarr::refresh_command_handler::ReadarrRefreshCommand;
|
||||||
|
use crate::cli::readarr::trigger_automatic_search_command_handler::ReadarrTriggerAutomaticSearchCommand;
|
||||||
use crate::models::readarr_models::{
|
use crate::models::readarr_models::{
|
||||||
BlocklistItem, BlocklistResponse, ReadarrSerdeable, ReadarrTaskName,
|
Author, BlocklistItem, BlocklistResponse, DeleteParams, ReadarrSerdeable, ReadarrTaskName,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
use crate::models::servarr_models::{IndexerSettings, ReleaseDownloadBody};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cli::{
|
cli::{
|
||||||
CliCommandHandler,
|
CliCommandHandler,
|
||||||
readarr::{ReadarrCliHandler, ReadarrCommand},
|
readarr::{
|
||||||
|
ReadarrCliHandler, ReadarrCommand, delete_command_handler::ReadarrDeleteCommand,
|
||||||
|
list_command_handler::ReadarrListCommand,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
models::Serdeable,
|
models::Serdeable,
|
||||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_readarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
|
||||||
|
let expected_tag_name = "test".to_owned();
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
ReadarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let add_tag_command = ReadarrCommand::Add(ReadarrAddCommand::Tag {
|
||||||
|
name: expected_tag_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = ReadarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_readarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(ReadarrEvent::GetStatus.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let get_system_status_command = ReadarrCommand::Get(ReadarrGetCommand::SystemStatus);
|
||||||
|
|
||||||
|
let result = ReadarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_readarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
|
||||||
|
let expected_delete_author_params = DeleteParams {
|
||||||
|
id: 7,
|
||||||
|
delete_files: true,
|
||||||
|
add_import_list_exclusion: false,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
ReadarrEvent::DeleteAuthor(expected_delete_author_params).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let delete_author_command = ReadarrCommand::Delete(ReadarrDeleteCommand::Author {
|
||||||
|
author_id: 7,
|
||||||
|
delete_files_from_disk: true,
|
||||||
|
add_list_exclusion: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = ReadarrCliHandler::with(&app_arc, delete_author_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_readarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
|
||||||
|
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||||
|
id: 1,
|
||||||
|
maximum_size: 26500,
|
||||||
|
minimum_age: 17,
|
||||||
|
retention: 43,
|
||||||
|
rss_sync_interval: 35,
|
||||||
|
};
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
ReadarrEvent::GetAllIndexerSettings.into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::IndexerSettings(
|
||||||
|
IndexerSettings {
|
||||||
|
id: 9,
|
||||||
|
maximum_size: 31200,
|
||||||
|
minimum_age: 22,
|
||||||
|
retention: 58,
|
||||||
|
rss_sync_interval: 90,
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
ReadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let edit_all_indexer_settings_command =
|
||||||
|
ReadarrCommand::Edit(ReadarrEditCommand::AllIndexerSettings {
|
||||||
|
maximum_size: Some(26500),
|
||||||
|
minimum_age: Some(17),
|
||||||
|
retention: Some(43),
|
||||||
|
rss_sync_interval: Some(35),
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = ReadarrCliHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
edit_all_indexer_settings_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_readarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(ReadarrEvent::ListAuthors.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::Authors(vec![
|
||||||
|
Author::default(),
|
||||||
|
])))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let list_authors_command = ReadarrCommand::List(ReadarrListCommand::Authors);
|
||||||
|
|
||||||
|
let result = ReadarrCliHandler::with(&app_arc, list_authors_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_readarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateAllAuthors.into()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let refresh_all_authors_command = ReadarrCommand::Refresh(ReadarrRefreshCommand::AllAuthors);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
ReadarrCliHandler::with(&app_arc, refresh_all_authors_command, &mut mock_network)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_readarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
|
||||||
|
{
|
||||||
|
let expected_author_id = 7;
|
||||||
|
let mut mock_network = MockNetworkTrait::new();
|
||||||
|
mock_network
|
||||||
|
.expect_handle_network_event()
|
||||||
|
.with(eq::<NetworkEvent>(
|
||||||
|
ReadarrEvent::TriggerAutomaticAuthorSearch(expected_author_id).into(),
|
||||||
|
))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||||
|
json!({"testResponse": "response"}),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||||
|
let trigger_automatic_search_command =
|
||||||
|
ReadarrCommand::TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand::Author {
|
||||||
|
author_id: expected_author_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = ReadarrCliHandler::with(
|
||||||
|
&app_arc,
|
||||||
|
trigger_automatic_search_command,
|
||||||
|
&mut mock_network,
|
||||||
|
)
|
||||||
|
.handle()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_ok!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_mark_history_item_as_failed_command() {
|
async fn test_mark_history_item_as_failed_command() {
|
||||||
let expected_history_item_id = 1234i64;
|
let expected_history_item_id = 1234i64;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ mod tests {
|
|||||||
#[case(Key::Char('q'), "q")]
|
#[case(Key::Char('q'), "q")]
|
||||||
#[case(Key::Ctrl('q'), "ctrl-q")]
|
#[case(Key::Ctrl('q'), "ctrl-q")]
|
||||||
fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) {
|
fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) {
|
||||||
assert_str_eq!(format!("{key}"), format!("{expected_str}"));
|
assert_str_eq!(format!("{key}"), expected_str.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ mod property_tests {
|
|||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||||
|
use crate::event::Key;
|
||||||
|
use crate::handlers::handle_events;
|
||||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::{
|
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::{
|
||||||
list_size, text_input_string,
|
list_size, text_input_string,
|
||||||
};
|
};
|
||||||
use crate::models::radarr_models::Movie;
|
use crate::models::radarr_models::Movie;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use crate::models::{Paginated, Scrollable};
|
use crate::models::{Paginated, Scrollable, strip_non_search_characters};
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
#[test]
|
#[test]
|
||||||
@@ -104,13 +107,29 @@ mod property_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_string_input_safety(input in text_input_string()) {
|
fn test_string_input_safety(input in text_input_string()) {
|
||||||
let _lowercase = input.to_lowercase();
|
let mut app = App::test_default();
|
||||||
let _uppercase = input.to_uppercase();
|
app.data.radarr_data.movies.set_items(vec![Movie::default()]);
|
||||||
let _trimmed = input.trim();
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||||
let _len = input.len();
|
handle_events(DEFAULT_KEYBINDINGS.filter.key, &mut app);
|
||||||
let _chars: Vec<char> = input.chars().collect();
|
|
||||||
|
|
||||||
prop_assert!(true);
|
for character in input.chars() {
|
||||||
|
handle_events(Key::Char(character), &mut app);
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_assert_eq!(app.get_current_route(), ActiveRadarrBlock::FilterMovies.into());
|
||||||
|
prop_assert_eq!(
|
||||||
|
app.data.radarr_data.movies.filter.as_ref().unwrap().text.as_str(),
|
||||||
|
input.as_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
for _ in 0..input.chars().count() {
|
||||||
|
handle_events(DEFAULT_KEYBINDINGS.backspace.key, &mut app);
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_assert_eq!(
|
||||||
|
app.data.radarr_data.movies.filter.as_ref().unwrap().text.as_str(),
|
||||||
|
""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -152,10 +171,19 @@ mod property_tests {
|
|||||||
table.set_items(movies);
|
table.set_items(movies);
|
||||||
|
|
||||||
for i in 0..page_ops {
|
for i in 0..page_ops {
|
||||||
|
let previous_index = table.state.selected().unwrap();
|
||||||
|
|
||||||
if i % 2 == 0 {
|
if i % 2 == 0 {
|
||||||
table.page_down();
|
table.page_down();
|
||||||
|
|
||||||
|
prop_assert_eq!(
|
||||||
|
table.state.selected(),
|
||||||
|
Some((previous_index + 20).min(list_size - 1))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
table.page_up();
|
table.page_up();
|
||||||
|
|
||||||
|
prop_assert_eq!(table.state.selected(), Some(previous_index.saturating_sub(20)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let current = table.current_selection();
|
let current = table.current_selection();
|
||||||
@@ -177,22 +205,36 @@ mod property_tests {
|
|||||||
..Movie::default()
|
..Movie::default()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
table.set_items(movies.clone());
|
table.set_items(movies.clone());
|
||||||
let original_size = table.items.len();
|
table.filter = Some(filter_term.clone().into());
|
||||||
|
let scrubbed_filter = strip_non_search_characters(&filter_term);
|
||||||
|
let matches_filter = |movie: &Movie| {
|
||||||
|
strip_non_search_characters(&movie.title.text).contains(&scrubbed_filter)
|
||||||
|
};
|
||||||
|
|
||||||
if !filter_term.is_empty() {
|
let has_match = table.apply_filter(|movie| &movie.title.text);
|
||||||
let filtered: Vec<Movie> = movies.into_iter()
|
|
||||||
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
|
|
||||||
.collect();
|
|
||||||
table.set_items(filtered);
|
|
||||||
}
|
|
||||||
|
|
||||||
prop_assert!(table.items.len() <= original_size);
|
prop_assert_eq!(&table.items, &movies);
|
||||||
|
prop_assert!(table.filter.is_none());
|
||||||
|
|
||||||
if !table.items.is_empty() {
|
if has_match {
|
||||||
let current = table.current_selection();
|
let filtered_items = table.filtered_items.as_ref().unwrap();
|
||||||
prop_assert!(current.id >= 0);
|
|
||||||
|
prop_assert!(!filtered_items.is_empty());
|
||||||
|
prop_assert!(filtered_items.len() <= movies.len());
|
||||||
|
|
||||||
|
for movie in filtered_items {
|
||||||
|
prop_assert!(matches_filter(movie));
|
||||||
|
}
|
||||||
|
|
||||||
|
for movie in movies.iter().filter(|movie| !filtered_items.contains(movie)) {
|
||||||
|
prop_assert!(!matches_filter(movie));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prop_assert!(table.filtered_items.is_none());
|
||||||
|
prop_assert!(
|
||||||
|
filter_term.is_empty() || movies.iter().all(|movie| !matches_filter(movie))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ mod tests {
|
|||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::Downloads,
|
ActiveLidarrBlock::Downloads,
|
||||||
ActiveLidarrBlock::DeleteDownloadPrompt,
|
ActiveLidarrBlock::DeleteDownloadPrompt,
|
||||||
LidarrEvent::DeleteDownload(1)
|
LidarrEvent::DeleteDownload(9)
|
||||||
)]
|
)]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::Downloads,
|
ActiveLidarrBlock::Downloads,
|
||||||
@@ -338,7 +338,7 @@ mod tests {
|
|||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::Downloads,
|
ActiveLidarrBlock::Downloads,
|
||||||
ActiveLidarrBlock::DeleteDownloadPrompt,
|
ActiveLidarrBlock::DeleteDownloadPrompt,
|
||||||
LidarrEvent::DeleteDownload(1)
|
LidarrEvent::DeleteDownload(9)
|
||||||
)]
|
)]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::Downloads,
|
ActiveLidarrBlock::Downloads,
|
||||||
@@ -423,7 +423,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.extract_download_id();
|
.extract_download_id();
|
||||||
|
|
||||||
assert_eq!(download_id, 1);
|
assert_eq!(download_id, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ mod tests {
|
|||||||
)]
|
)]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::DeleteTrackFilePrompt,
|
ActiveLidarrBlock::DeleteTrackFilePrompt,
|
||||||
LidarrEvent::DeleteTrackFile(1)
|
LidarrEvent::DeleteTrackFile(7)
|
||||||
)]
|
)]
|
||||||
fn test_album_details_prompt_confirm_submit(
|
fn test_album_details_prompt_confirm_submit(
|
||||||
#[case] prompt_block: ActiveLidarrBlock,
|
#[case] prompt_block: ActiveLidarrBlock,
|
||||||
@@ -606,7 +606,7 @@ mod tests {
|
|||||||
)]
|
)]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::DeleteTrackFilePrompt,
|
ActiveLidarrBlock::DeleteTrackFilePrompt,
|
||||||
LidarrEvent::DeleteTrackFile(1)
|
LidarrEvent::DeleteTrackFile(7)
|
||||||
)]
|
)]
|
||||||
fn test_album_details_prompt_confirm_confirm_key(
|
fn test_album_details_prompt_confirm_confirm_key(
|
||||||
#[case] prompt_block: ActiveLidarrBlock,
|
#[case] prompt_block: ActiveLidarrBlock,
|
||||||
@@ -752,7 +752,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.extract_track_file_id();
|
.extract_track_file_id();
|
||||||
|
|
||||||
assert_eq!(track_file_id, 1);
|
assert_eq!(track_file_id, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -768,7 +768,7 @@ mod tests {
|
|||||||
mod test_handle_submit {
|
mod test_handle_submit {
|
||||||
use crate::assert_navigation_popped;
|
use crate::assert_navigation_popped;
|
||||||
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::{
|
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::{
|
||||||
add_movie_body, add_movie_search_result, collection_movie,
|
add_movie_search_result, collection_movie,
|
||||||
};
|
};
|
||||||
use crate::models::BlockSelectionState;
|
use crate::models::BlockSelectionState;
|
||||||
use crate::models::radarr_models::Movie;
|
use crate::models::radarr_models::Movie;
|
||||||
@@ -1336,7 +1336,7 @@ mod tests {
|
|||||||
use crate::{
|
use crate::{
|
||||||
assert_navigation_popped,
|
assert_navigation_popped,
|
||||||
handlers::radarr_handlers::radarr_handler_test_utils::utils::{
|
handlers::radarr_handlers::radarr_handler_test_utils::utils::{
|
||||||
add_movie_body, add_movie_search_result, collection_movie,
|
add_movie_search_result, collection_movie,
|
||||||
},
|
},
|
||||||
models::{
|
models::{
|
||||||
BlockSelectionState,
|
BlockSelectionState,
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
|||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie,
|
AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie,
|
||||||
DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection,
|
DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection,
|
||||||
MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList,
|
MovieFile, MovieMonitor, MovieStatus, RadarrRelease, Rating, RatingsList,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
DownloadStatus, Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||||
};
|
};
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use serde_json::{Number, json};
|
use serde_json::{Number, json};
|
||||||
@@ -329,7 +329,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
|||||||
title: "Test".to_owned().into(),
|
title: "Test".to_owned().into(),
|
||||||
original_language: language(),
|
original_language: language(),
|
||||||
size_on_disk: 3543348019,
|
size_on_disk: 3543348019,
|
||||||
status: "Downloaded".to_owned(),
|
status: MovieStatus::Released,
|
||||||
overview: "Blah blah blah".to_owned(),
|
overview: "Blah blah blah".to_owned(),
|
||||||
path: "/nfs/movies".to_owned(),
|
path: "/nfs/movies".to_owned(),
|
||||||
studio: Some("21st Century Alex".to_owned()),
|
studio: Some("21st Century Alex".to_owned()),
|
||||||
@@ -395,7 +395,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
|||||||
tmdb_id: 4321,
|
tmdb_id: 4321,
|
||||||
title: HorizontallyScrollableText::from("Test Search Result"),
|
title: HorizontallyScrollableText::from("Test Search Result"),
|
||||||
original_language: language(),
|
original_language: language(),
|
||||||
status: "released".to_owned(),
|
status: MovieStatus::Released,
|
||||||
overview: "New movie blah blah blah".to_owned(),
|
overview: "New movie blah blah blah".to_owned(),
|
||||||
genres: genres(),
|
genres: genres(),
|
||||||
year: 2023,
|
year: 2023,
|
||||||
@@ -407,7 +407,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
|||||||
pub fn download_record() -> DownloadRecord {
|
pub fn download_record() -> DownloadRecord {
|
||||||
DownloadRecord {
|
DownloadRecord {
|
||||||
title: "Test Download Title".to_owned(),
|
title: "Test Download Title".to_owned(),
|
||||||
status: "downloading".to_owned(),
|
status: DownloadStatus::Downloading,
|
||||||
id: 1,
|
id: 1,
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
size: 3543348019,
|
size: 3543348019,
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ mod tests {
|
|||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::handlers::KeyEventHandler;
|
use crate::handlers::KeyEventHandler;
|
||||||
use crate::handlers::readarr_handlers::downloads::DownloadsHandler;
|
use crate::handlers::readarr_handlers::downloads::DownloadsHandler;
|
||||||
use crate::models::readarr_models::{DownloadRecord, DownloadStatus};
|
use crate::models::readarr_models::DownloadRecord;
|
||||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, DOWNLOADS_BLOCKS};
|
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, DOWNLOADS_BLOCKS};
|
||||||
|
use crate::models::servarr_models::DownloadStatus;
|
||||||
|
|
||||||
mod test_handle_delete {
|
mod test_handle_delete {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ impl AddAuthorHandler<'_, '_> {
|
|||||||
quality_profile_id,
|
quality_profile_id,
|
||||||
metadata_profile_id,
|
metadata_profile_id,
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
|
images: Vec::new(),
|
||||||
tag_input_string: Some(tags),
|
tag_input_string: Some(tags),
|
||||||
add_options: AddAuthorOptions {
|
add_options: AddAuthorOptions {
|
||||||
monitor,
|
monitor,
|
||||||
|
|||||||
@@ -1237,6 +1237,7 @@ mod tests {
|
|||||||
quality_profile_id: 2222,
|
quality_profile_id: 2222,
|
||||||
metadata_profile_id: 4444,
|
metadata_profile_id: 4444,
|
||||||
tags: Vec::default(),
|
tags: Vec::default(),
|
||||||
|
images: Vec::new(),
|
||||||
tag_input_string: Some("usenet, testing".to_owned()),
|
tag_input_string: Some("usenet, testing".to_owned()),
|
||||||
add_options: AddAuthorOptions {
|
add_options: AddAuthorOptions {
|
||||||
monitor: MonitorType::Future,
|
monitor: MonitorType::Future,
|
||||||
@@ -1628,6 +1629,7 @@ mod tests {
|
|||||||
quality_profile_id: 2222,
|
quality_profile_id: 2222,
|
||||||
metadata_profile_id: 4444,
|
metadata_profile_id: 4444,
|
||||||
tags: Vec::default(),
|
tags: Vec::default(),
|
||||||
|
images: Vec::new(),
|
||||||
tag_input_string: Some("usenet, testing".to_owned()),
|
tag_input_string: Some("usenet, testing".to_owned()),
|
||||||
add_options: AddAuthorOptions {
|
add_options: AddAuthorOptions {
|
||||||
monitor: MonitorType::Future,
|
monitor: MonitorType::Future,
|
||||||
@@ -1795,6 +1797,7 @@ mod tests {
|
|||||||
quality_profile_id: 2222,
|
quality_profile_id: 2222,
|
||||||
metadata_profile_id: 4444,
|
metadata_profile_id: 4444,
|
||||||
tags: Vec::default(),
|
tags: Vec::default(),
|
||||||
|
images: Vec::new(),
|
||||||
tag_input_string: Some("usenet, testing".to_owned()),
|
tag_input_string: Some("usenet, testing".to_owned()),
|
||||||
add_options: AddAuthorOptions {
|
add_options: AddAuthorOptions {
|
||||||
monitor: MonitorType::Future,
|
monitor: MonitorType::Future,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ mod tests {
|
|||||||
#[case(
|
#[case(
|
||||||
ActiveSonarrBlock::Downloads,
|
ActiveSonarrBlock::Downloads,
|
||||||
ActiveSonarrBlock::DeleteDownloadPrompt,
|
ActiveSonarrBlock::DeleteDownloadPrompt,
|
||||||
SonarrEvent::DeleteDownload(1)
|
SonarrEvent::DeleteDownload(9)
|
||||||
)]
|
)]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveSonarrBlock::Downloads,
|
ActiveSonarrBlock::Downloads,
|
||||||
@@ -340,7 +340,7 @@ mod tests {
|
|||||||
#[case(
|
#[case(
|
||||||
ActiveSonarrBlock::Downloads,
|
ActiveSonarrBlock::Downloads,
|
||||||
ActiveSonarrBlock::DeleteDownloadPrompt,
|
ActiveSonarrBlock::DeleteDownloadPrompt,
|
||||||
SonarrEvent::DeleteDownload(1)
|
SonarrEvent::DeleteDownload(9)
|
||||||
)]
|
)]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveSonarrBlock::Downloads,
|
ActiveSonarrBlock::Downloads,
|
||||||
@@ -431,7 +431,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.extract_download_id();
|
.extract_download_id();
|
||||||
|
|
||||||
assert_eq!(download_id, 1);
|
assert_eq!(download_id, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ mod tests {
|
|||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveSonarrBlock::AutomaticallySearchEpisodePrompt,
|
ActiveSonarrBlock::AutomaticallySearchEpisodePrompt,
|
||||||
SonarrEvent::TriggerAutomaticEpisodeSearch(1)
|
SonarrEvent::TriggerAutomaticEpisodeSearch(11)
|
||||||
)]
|
)]
|
||||||
fn test_episode_details_prompt_confirm_submit(
|
fn test_episode_details_prompt_confirm_submit(
|
||||||
#[case] prompt_block: ActiveSonarrBlock,
|
#[case] prompt_block: ActiveSonarrBlock,
|
||||||
@@ -646,7 +646,7 @@ mod tests {
|
|||||||
assert_navigation_popped!(app, active_sonarr_block.into());
|
assert_navigation_popped!(app, active_sonarr_block.into());
|
||||||
assert_some_eq_x!(
|
assert_some_eq_x!(
|
||||||
&app.data.sonarr_data.prompt_confirm_action,
|
&app.data.sonarr_data.prompt_confirm_action,
|
||||||
&SonarrEvent::TriggerAutomaticEpisodeSearch(1)
|
&SonarrEvent::TriggerAutomaticEpisodeSearch(11)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,7 +787,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.extract_episode_id();
|
.extract_episode_id();
|
||||||
|
|
||||||
assert_eq!(episode_id, 1);
|
assert_eq!(episode_id, 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -673,7 +673,7 @@ mod tests {
|
|||||||
assert!(app.is_routing);
|
assert!(app.is_routing);
|
||||||
assert_some_eq_x!(
|
assert_some_eq_x!(
|
||||||
&app.data.sonarr_data.prompt_confirm_action,
|
&app.data.sonarr_data.prompt_confirm_action,
|
||||||
&SonarrEvent::ToggleEpisodeMonitoring(1)
|
&SonarrEvent::ToggleEpisodeMonitoring(11)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1129,7 +1129,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.extract_episode_id();
|
.extract_episode_id();
|
||||||
|
|
||||||
assert_eq!(episode_id, 1);
|
assert_eq!(episode_id, 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ pub(in crate::handlers::sonarr_handlers) mod utils {
|
|||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
use crate::models::servarr_models::IndexerSettings;
|
use crate::models::servarr_models::IndexerSettings;
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
DownloadStatus, Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||||
};
|
};
|
||||||
use crate::models::sonarr_models::{
|
use crate::models::sonarr_models::{
|
||||||
AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadRecord, DownloadStatus,
|
AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadRecord, Episode, EpisodeFile,
|
||||||
Episode, EpisodeFile, MediaInfo, Rating, Season, SeasonStatistics, Series, SeriesStatistics,
|
MediaInfo, Rating, Season, SeasonStatistics, Series, SeriesStatistics, SeriesStatus,
|
||||||
SeriesStatus, SeriesType,
|
SeriesType,
|
||||||
};
|
};
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use serde_json::{Number, json};
|
use serde_json::{Number, json};
|
||||||
@@ -191,8 +191,8 @@ pub(in crate::handlers::sonarr_handlers) mod utils {
|
|||||||
DownloadRecord {
|
DownloadRecord {
|
||||||
title: "Test Download Title".to_owned(),
|
title: "Test Download Title".to_owned(),
|
||||||
status: DownloadStatus::Downloading,
|
status: DownloadStatus::Downloading,
|
||||||
id: 1,
|
id: 9,
|
||||||
episode_id: Some(Number::from(1i64)),
|
episode_id: Some(Number::from(11i64)),
|
||||||
size: 3543348019f64,
|
size: 3543348019f64,
|
||||||
sizeleft: 1771674009f64,
|
sizeleft: 1771674009f64,
|
||||||
output_path: Some(HorizontallyScrollableText::from(
|
output_path: Some(HorizontallyScrollableText::from(
|
||||||
@@ -205,10 +205,10 @@ pub(in crate::handlers::sonarr_handlers) mod utils {
|
|||||||
|
|
||||||
pub fn episode() -> Episode {
|
pub fn episode() -> Episode {
|
||||||
Episode {
|
Episode {
|
||||||
id: 1,
|
id: 11,
|
||||||
series_id: 1,
|
series_id: 1,
|
||||||
tvdb_id: 1234,
|
tvdb_id: 1234,
|
||||||
episode_file_id: 1,
|
episode_file_id: 7,
|
||||||
season_number: 1,
|
season_number: 1,
|
||||||
episode_number: 1,
|
episode_number: 1,
|
||||||
title: "Something cool".to_owned(),
|
title: "Something cool".to_owned(),
|
||||||
@@ -224,7 +224,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils {
|
|||||||
|
|
||||||
pub fn episode_file() -> EpisodeFile {
|
pub fn episode_file() -> EpisodeFile {
|
||||||
EpisodeFile {
|
EpisodeFile {
|
||||||
id: 1,
|
id: 7,
|
||||||
relative_path: "/season 1/episode 1.mkv".to_owned(),
|
relative_path: "/season 1/episode 1.mkv".to_owned(),
|
||||||
path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(),
|
path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(),
|
||||||
size: 3543348019,
|
size: 3543348019,
|
||||||
|
|||||||
@@ -606,17 +606,18 @@ mod tests {
|
|||||||
.take(100)
|
.take(100)
|
||||||
.collect();
|
.collect();
|
||||||
app.data.radarr_data.movies.set_items(movies_vec);
|
app.data.radarr_data.movies.set_items(movies_vec);
|
||||||
|
app.data.radarr_data.movies.state.select(Some(50));
|
||||||
TableHandlerUnit::new(key, &mut app, ActiveRadarrBlock::Movies, None).handle();
|
TableHandlerUnit::new(key, &mut app, ActiveRadarrBlock::Movies, None).handle();
|
||||||
|
|
||||||
if key == Key::PgUp {
|
if key == Key::PgUp {
|
||||||
assert_str_eq!(
|
assert_str_eq!(
|
||||||
app.data.radarr_data.movies.current_selection().title.text,
|
app.data.radarr_data.movies.current_selection().title.text,
|
||||||
"Test 79"
|
"Test 30"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert_str_eq!(
|
assert_str_eq!(
|
||||||
app.data.radarr_data.movies.current_selection().title.text,
|
app.data.radarr_data.movies.current_selection().title.text,
|
||||||
"Test 20"
|
"Test 70"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,13 +171,40 @@ async fn main() -> Result<()> {
|
|||||||
cancellation_token.clone(),
|
cancellation_token.clone(),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
if args.command.is_none()
|
||||||
|
&& let Some(name) = args.global.servarr_name.as_deref()
|
||||||
|
{
|
||||||
|
let mut app = app.lock().await;
|
||||||
|
anyhow::ensure!(
|
||||||
|
app.server_tabs.select_tab_by_title(name.trim()),
|
||||||
|
"A Servarr titled '{}' was not found in your configuration file",
|
||||||
|
name.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(command) => match command {
|
Some(command) => match command {
|
||||||
Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) | Command::Readarr(_) => {
|
Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) | Command::Readarr(_) => {
|
||||||
if spinner_disabled {
|
if spinner_disabled {
|
||||||
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
start_cli_no_spinner(
|
||||||
|
config,
|
||||||
|
reqwest_client,
|
||||||
|
cancellation_token,
|
||||||
|
app,
|
||||||
|
command,
|
||||||
|
args.global.servarr_name.as_deref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
} else {
|
} else {
|
||||||
start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await;
|
start_cli_with_spinner(
|
||||||
|
config,
|
||||||
|
reqwest_client,
|
||||||
|
cancellation_token,
|
||||||
|
app,
|
||||||
|
command,
|
||||||
|
args.global.servarr_name.as_deref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Completions { shell } => {
|
Command::Completions { shell } => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use super::{
|
use super::{
|
||||||
HorizontallyScrollableText, Serdeable,
|
HorizontallyScrollableText, Serdeable,
|
||||||
servarr_models::{
|
servarr_models::{
|
||||||
DiskSpace, HostConfig, Indexer, IndexerTestResult, MetadataProfile, QualityProfile,
|
DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerTestResult, MetadataProfile,
|
||||||
QualityWrapper, RootFolder, SecurityConfig, SystemStatus, Tag,
|
QualityProfile, QualityWrapper, RootFolder, SecurityConfig, SystemStatus, Tag,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{IndexerSettings, LogResponse, QueueEvent, Update};
|
use crate::models::servarr_models::{IndexerSettings, LogResponse, QueueEvent, Update};
|
||||||
@@ -27,6 +27,7 @@ pub struct Artist {
|
|||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub artist_name: HorizontallyScrollableText,
|
pub artist_name: HorizontallyScrollableText,
|
||||||
pub foreign_artist_id: String,
|
pub foreign_artist_id: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: ArtistStatus,
|
pub status: ArtistStatus,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
pub artist_type: Option<String>,
|
pub artist_type: Option<String>,
|
||||||
@@ -38,6 +39,7 @@ pub struct Artist {
|
|||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub metadata_profile_id: i64,
|
pub metadata_profile_id: i64,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub monitor_new_items: NewItemMonitorType,
|
pub monitor_new_items: NewItemMonitorType,
|
||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
pub tags: Vec<Number>,
|
pub tags: Vec<Number>,
|
||||||
@@ -161,6 +163,7 @@ pub enum MonitorType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadRecord {
|
pub struct DownloadRecord {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: DownloadStatus,
|
pub status: DownloadStatus,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -178,36 +181,6 @@ pub struct DownloadRecord {
|
|||||||
|
|
||||||
impl Eq for DownloadRecord {}
|
impl Eq for DownloadRecord {}
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
Default,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
Debug,
|
|
||||||
EnumIter,
|
|
||||||
Display,
|
|
||||||
EnumDisplayStyle,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[strum(serialize_all = "camelCase")]
|
|
||||||
pub enum DownloadStatus {
|
|
||||||
#[default]
|
|
||||||
Unknown,
|
|
||||||
Queued,
|
|
||||||
Paused,
|
|
||||||
Downloading,
|
|
||||||
Completed,
|
|
||||||
Failed,
|
|
||||||
Warning,
|
|
||||||
Delay,
|
|
||||||
#[display_style(name = "Download Client Unavailable")]
|
|
||||||
DownloadClientUnavailable,
|
|
||||||
Fallback,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadsResponse {
|
pub struct DownloadsResponse {
|
||||||
@@ -219,6 +192,7 @@ pub struct DownloadsResponse {
|
|||||||
pub struct AddArtistSearchResult {
|
pub struct AddArtistSearchResult {
|
||||||
pub foreign_artist_id: String,
|
pub foreign_artist_id: String,
|
||||||
pub artist_name: HorizontallyScrollableText,
|
pub artist_name: HorizontallyScrollableText,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: ArtistStatus,
|
pub status: ArtistStatus,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
pub artist_type: Option<String>,
|
pub artist_type: Option<String>,
|
||||||
@@ -414,6 +388,7 @@ pub struct LidarrHistoryItem {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LidarrTask {
|
pub struct LidarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub task_name: LidarrTaskName,
|
pub task_name: LidarrTaskName,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub interval: i64,
|
pub interval: i64,
|
||||||
|
|||||||
@@ -2,23 +2,27 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::lidarr_models::{
|
use crate::models::lidarr_models::{
|
||||||
AddArtistSearchResult, Album, AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord,
|
AddArtistSearchResult, Album, AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord,
|
||||||
DownloadStatus, DownloadsResponse, LidarrHistoryEventType, LidarrHistoryItem,
|
DownloadsResponse, LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper,
|
||||||
LidarrHistoryWrapper, LidarrRelease, LidarrTask, MediaInfo, Member, MonitorType,
|
LidarrRelease, LidarrTask, LidarrTaskName, MediaInfo, Member, MonitorType, NewItemMonitorType,
|
||||||
NewItemMonitorType, Track, TrackFile,
|
Track, TrackFile,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
|
DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log,
|
||||||
MetadataProfile, QualityProfile, QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag,
|
LogResponse, MetadataProfile, QualityProfile, QueueEvent, RootFolder, SecurityConfig,
|
||||||
Update,
|
SystemStatus, Tag, Update,
|
||||||
};
|
};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
Serdeable,
|
Serdeable,
|
||||||
lidarr_models::{Artist, ArtistStatistics, ArtistStatus, LidarrSerdeable, Ratings},
|
lidarr_models::{Artist, ArtistStatistics, ArtistStatus, LidarrSerdeable, Ratings},
|
||||||
};
|
};
|
||||||
|
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||||
|
ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON, download_record, task,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_artist_status_default() {
|
fn test_artist_status_default() {
|
||||||
@@ -655,40 +659,6 @@ mod tests {
|
|||||||
assert_str_eq!(ArtistStatus::Deleted.to_display_str(), "Deleted");
|
assert_str_eq!(ArtistStatus::Deleted.to_display_str(), "Deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_download_status_display() {
|
|
||||||
assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown");
|
|
||||||
assert_str_eq!(DownloadStatus::Queued.to_string(), "queued");
|
|
||||||
assert_str_eq!(DownloadStatus::Paused.to_string(), "paused");
|
|
||||||
assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading");
|
|
||||||
assert_str_eq!(DownloadStatus::Completed.to_string(), "completed");
|
|
||||||
assert_str_eq!(DownloadStatus::Failed.to_string(), "failed");
|
|
||||||
assert_str_eq!(DownloadStatus::Warning.to_string(), "warning");
|
|
||||||
assert_str_eq!(DownloadStatus::Delay.to_string(), "delay");
|
|
||||||
assert_str_eq!(
|
|
||||||
DownloadStatus::DownloadClientUnavailable.to_string(),
|
|
||||||
"downloadClientUnavailable"
|
|
||||||
);
|
|
||||||
assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_download_status_to_display_str() {
|
|
||||||
assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown");
|
|
||||||
assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued");
|
|
||||||
assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused");
|
|
||||||
assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading");
|
|
||||||
assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed");
|
|
||||||
assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed");
|
|
||||||
assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning");
|
|
||||||
assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay");
|
|
||||||
assert_str_eq!(
|
|
||||||
DownloadStatus::DownloadClientUnavailable.to_display_str(),
|
|
||||||
"Download Client Unavailable"
|
|
||||||
);
|
|
||||||
assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lidarr_history_event_type_display() {
|
fn test_lidarr_history_event_type_display() {
|
||||||
assert_str_eq!(LidarrHistoryEventType::Unknown.to_string(), "unknown");
|
assert_str_eq!(LidarrHistoryEventType::Unknown.to_string(), "unknown");
|
||||||
@@ -946,6 +916,108 @@ mod tests {
|
|||||||
assert_none!(&search_result.ratings);
|
assert_none!(&search_result.ratings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_artist_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Artist {
|
||||||
|
status: ArtistStatus::default(),
|
||||||
|
..serde_json::from_str(ARTIST_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Artist>(ARTIST_JSON, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Artist>(ARTIST_JSON, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_artist_monitor_new_items_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Artist {
|
||||||
|
monitor_new_items: NewItemMonitorType::default(),
|
||||||
|
..serde_json::from_str(ARTIST_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Artist>(ARTIST_JSON, "monitorNewItems", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Artist>(ARTIST_JSON, "monitorNewItems", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_search_result_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = AddArtistSearchResult {
|
||||||
|
status: ArtistStatus::default(),
|
||||||
|
..serde_json::from_str(ADD_ARTIST_SEARCH_RESULT_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddArtistSearchResult>(
|
||||||
|
ADD_ARTIST_SEARCH_RESULT_JSON,
|
||||||
|
"status",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddArtistSearchResult>(
|
||||||
|
ADD_ARTIST_SEARCH_RESULT_JSON,
|
||||||
|
"status",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_record_status_deserialization_falls_back_to_default() {
|
||||||
|
let download_record_json = serde_json::to_string(&download_record()).unwrap();
|
||||||
|
let expected = DownloadRecord {
|
||||||
|
status: DownloadStatus::default(),
|
||||||
|
..download_record()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lidarr_task_task_name_deserialization_falls_back_to_default() {
|
||||||
|
let task_json = serde_json::to_string(&task()).unwrap();
|
||||||
|
let expected = LidarrTask {
|
||||||
|
task_name: LidarrTaskName::default(),
|
||||||
|
..task()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<LidarrTask>(&task_json, "taskName", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<LidarrTask>(&task_json, "taskName", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_history_event_type(event_type: Value) -> LidarrHistoryEventType {
|
fn deserialize_history_event_type(event_type: Value) -> LidarrHistoryEventType {
|
||||||
let history_item_json = json!({
|
let history_item_json = json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use strum_macros::{Display, EnumIter};
|
|||||||
|
|
||||||
use super::Serdeable;
|
use super::Serdeable;
|
||||||
use super::servarr_models::{
|
use super::servarr_models::{
|
||||||
DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper,
|
DiskSpace, DownloadStatus, HostConfig, Indexer, Language, LogResponse, QualityProfile,
|
||||||
QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag, Update,
|
QualityWrapper, QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag, Update,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -42,7 +42,8 @@ pub struct AddMovieSearchResult {
|
|||||||
pub tmdb_id: i64,
|
pub tmdb_id: i64,
|
||||||
pub title: HorizontallyScrollableText,
|
pub title: HorizontallyScrollableText,
|
||||||
pub original_language: Language,
|
pub original_language: Language,
|
||||||
pub status: String,
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
|
pub status: MovieStatus,
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
@@ -98,6 +99,7 @@ pub struct Collection {
|
|||||||
pub search_on_add: bool,
|
pub search_on_add: bool,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub minimum_availability: MinimumAvailability,
|
pub minimum_availability: MinimumAvailability,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub quality_profile_id: i64,
|
pub quality_profile_id: i64,
|
||||||
@@ -126,7 +128,7 @@ pub struct Credit {
|
|||||||
pub character: Option<String>,
|
pub character: Option<String>,
|
||||||
pub department: Option<String>,
|
pub department: Option<String>,
|
||||||
pub job: Option<String>,
|
pub job: Option<String>,
|
||||||
#[serde(rename(deserialize = "type"))]
|
#[serde(rename = "type", deserialize_with = "super::from_json_or_default")]
|
||||||
pub credit_type: CreditType,
|
pub credit_type: CreditType,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +152,8 @@ pub struct DeleteMovieParams {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadRecord {
|
pub struct DownloadRecord {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub status: String,
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
|
pub status: DownloadStatus,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
@@ -301,7 +304,8 @@ pub struct Movie {
|
|||||||
pub original_language: Language,
|
pub original_language: Language,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub size_on_disk: i64,
|
pub size_on_disk: i64,
|
||||||
pub status: String,
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
|
pub status: MovieStatus,
|
||||||
pub overview: String,
|
pub overview: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub studio: Option<String>,
|
pub studio: Option<String>,
|
||||||
@@ -316,6 +320,7 @@ pub struct Movie {
|
|||||||
pub tmdb_id: i64,
|
pub tmdb_id: i64,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub quality_profile_id: i64,
|
pub quality_profile_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub minimum_availability: MinimumAvailability,
|
pub minimum_availability: MinimumAvailability,
|
||||||
pub certification: Option<String>,
|
pub certification: Option<String>,
|
||||||
pub tags: Vec<Number>,
|
pub tags: Vec<Number>,
|
||||||
@@ -324,6 +329,32 @@ pub struct Movie {
|
|||||||
pub collection: Option<MovieCollection>,
|
pub collection: Option<MovieCollection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
Default,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
EnumIter,
|
||||||
|
Display,
|
||||||
|
EnumDisplayStyle,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[strum(serialize_all = "camelCase")]
|
||||||
|
pub enum MovieStatus {
|
||||||
|
#[default]
|
||||||
|
#[display_style(name = "TBA")]
|
||||||
|
Tba,
|
||||||
|
Announced,
|
||||||
|
#[display_style(name = "In Cinemas")]
|
||||||
|
InCinemas,
|
||||||
|
Released,
|
||||||
|
Deleted,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MovieCollection {
|
pub struct MovieCollection {
|
||||||
@@ -469,6 +500,7 @@ pub struct RadarrHistoryItem {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RadarrTask {
|
pub struct RadarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub task_name: RadarrTaskName,
|
pub task_name: RadarrTaskName,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub interval: i64,
|
pub interval: i64,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
@@ -9,15 +10,19 @@ mod tests {
|
|||||||
use crate::models::{
|
use crate::models::{
|
||||||
Serdeable,
|
Serdeable,
|
||||||
radarr_models::{
|
radarr_models::{
|
||||||
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, DiskSpace,
|
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, CreditType,
|
||||||
DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult,
|
DiskSpace, DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult,
|
||||||
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, QualityProfile, RadarrRelease,
|
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, MovieStatus, QualityProfile,
|
||||||
RadarrSerdeable, RadarrTask, RadarrTaskName, Tag, Update,
|
RadarrRelease, RadarrSerdeable, RadarrTask, RadarrTaskName, Tag, Update,
|
||||||
},
|
},
|
||||||
servarr_models::{
|
servarr_models::{
|
||||||
HostConfig, Log, LogResponse, QueueEvent, RootFolder, SecurityConfig, SystemStatus,
|
DownloadStatus, HostConfig, Log, LogResponse, QueueEvent, RootFolder, SecurityConfig,
|
||||||
|
SystemStatus,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use crate::network::radarr_network::radarr_network_test_utils::test_utils::{
|
||||||
|
MOVIE_JSON, add_movie_search_result, collection, crew_credit, download_record, task,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_task_name_display() {
|
fn test_task_name_display() {
|
||||||
@@ -46,6 +51,24 @@ mod tests {
|
|||||||
assert_str_eq!(MinimumAvailability::Released.to_display_str(), "Released");
|
assert_str_eq!(MinimumAvailability::Released.to_display_str(), "Released");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_movie_status_display() {
|
||||||
|
assert_str_eq!(MovieStatus::Tba.to_string(), "tba");
|
||||||
|
assert_str_eq!(MovieStatus::Announced.to_string(), "announced");
|
||||||
|
assert_str_eq!(MovieStatus::InCinemas.to_string(), "inCinemas");
|
||||||
|
assert_str_eq!(MovieStatus::Released.to_string(), "released");
|
||||||
|
assert_str_eq!(MovieStatus::Deleted.to_string(), "deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_movie_status_to_display_str() {
|
||||||
|
assert_str_eq!(MovieStatus::Tba.to_display_str(), "TBA");
|
||||||
|
assert_str_eq!(MovieStatus::Announced.to_display_str(), "Announced");
|
||||||
|
assert_str_eq!(MovieStatus::InCinemas.to_display_str(), "In Cinemas");
|
||||||
|
assert_str_eq!(MovieStatus::Released.to_display_str(), "Released");
|
||||||
|
assert_str_eq!(MovieStatus::Deleted.to_display_str(), "Deleted");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_monitor_display() {
|
fn test_monitor_display() {
|
||||||
assert_str_eq!(MovieMonitor::MovieOnly.to_string(), "movieOnly");
|
assert_str_eq!(MovieMonitor::MovieOnly.to_string(), "movieOnly");
|
||||||
@@ -222,11 +245,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_credit_serializes_credit_type_under_the_type_key() {
|
||||||
|
let credit_json = serde_json::to_value(crew_credit()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(credit_json["type"], json!("crew"));
|
||||||
|
assert_none!(credit_json.get("creditType"));
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_value::<Credit>(credit_json).unwrap(),
|
||||||
|
crew_credit()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_download_record_default_indexer_value() {
|
fn test_download_record_default_indexer_value() {
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
"title": "test",
|
"title": "test",
|
||||||
"status": "test",
|
"status": "downloading",
|
||||||
"id": 0,
|
"id": 0,
|
||||||
"movieId": 0,
|
"movieId": 0,
|
||||||
"size": 0,
|
"size": 0,
|
||||||
@@ -235,7 +270,7 @@ mod tests {
|
|||||||
}"#;
|
}"#;
|
||||||
let expected_record = DownloadRecord {
|
let expected_record = DownloadRecord {
|
||||||
title: "test".to_owned(),
|
title: "test".to_owned(),
|
||||||
status: "test".to_owned(),
|
status: DownloadStatus::Downloading,
|
||||||
id: 0,
|
id: 0,
|
||||||
movie_id: 0,
|
movie_id: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
@@ -614,6 +649,149 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_movie_minimum_availability_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Movie {
|
||||||
|
minimum_availability: MinimumAvailability::default(),
|
||||||
|
..serde_json::from_str(MOVIE_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Movie>(MOVIE_JSON, "minimumAvailability", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Movie>(MOVIE_JSON, "minimumAvailability", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_movie_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Movie {
|
||||||
|
status: MovieStatus::default(),
|
||||||
|
..serde_json::from_str(MOVIE_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Movie>(MOVIE_JSON, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Movie>(MOVIE_JSON, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_movie_search_result_status_deserialization_falls_back_to_default() {
|
||||||
|
let add_movie_search_result_json = serde_json::to_string(&add_movie_search_result()).unwrap();
|
||||||
|
let expected = AddMovieSearchResult {
|
||||||
|
status: MovieStatus::default(),
|
||||||
|
..add_movie_search_result()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddMovieSearchResult>(
|
||||||
|
&add_movie_search_result_json,
|
||||||
|
"status",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddMovieSearchResult>(
|
||||||
|
&add_movie_search_result_json,
|
||||||
|
"status",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collection_minimum_availability_deserialization_falls_back_to_default() {
|
||||||
|
let collection_json = serde_json::to_string(&collection()).unwrap();
|
||||||
|
let expected = Collection {
|
||||||
|
minimum_availability: MinimumAvailability::default(),
|
||||||
|
..collection()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Collection>(&collection_json, "minimumAvailability", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Collection>(
|
||||||
|
&collection_json,
|
||||||
|
"minimumAvailability",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_credit_credit_type_deserialization_falls_back_to_default() {
|
||||||
|
let credit_json = serde_json::to_string(&crew_credit()).unwrap();
|
||||||
|
let expected = Credit {
|
||||||
|
credit_type: CreditType::default(),
|
||||||
|
..crew_credit()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Credit>(&credit_json, "type", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Credit>(&credit_json, "type", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_radarr_task_task_name_deserialization_falls_back_to_default() {
|
||||||
|
let task_json = serde_json::to_string(&task()).unwrap();
|
||||||
|
let expected = RadarrTask {
|
||||||
|
task_name: RadarrTaskName::default(),
|
||||||
|
..task()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<RadarrTask>(&task_json, "taskName", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<RadarrTask>(&task_json, "taskName", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_record_status_deserialization_falls_back_to_default() {
|
||||||
|
let download_record_json = serde_json::to_string(&download_record()).unwrap();
|
||||||
|
let expected = DownloadRecord {
|
||||||
|
status: DownloadStatus::default(),
|
||||||
|
..download_record()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_history_event_type(event_type: Value) -> RadarrHistoryEventType {
|
fn deserialize_history_event_type(event_type: Value) -> RadarrHistoryEventType {
|
||||||
let history_item_json = json!({
|
let history_item_json = json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use super::{
|
use super::{
|
||||||
HorizontallyScrollableText, Serdeable,
|
HorizontallyScrollableText, Serdeable,
|
||||||
servarr_models::{
|
servarr_models::{
|
||||||
DiskSpace, HostConfig, Indexer, IndexerTestResult, MetadataProfile, QualityProfile,
|
DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerTestResult, MetadataProfile,
|
||||||
QualityWrapper, RootFolder, SecurityConfig, SystemStatus, Tag,
|
QualityProfile, QualityWrapper, RootFolder, SecurityConfig, SystemStatus, Tag,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{IndexerSettings, LogResponse, QueueEvent, Update};
|
use crate::models::servarr_models::{IndexerSettings, LogResponse, QueueEvent, Update};
|
||||||
@@ -27,6 +27,7 @@ pub struct Author {
|
|||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub author_name: HorizontallyScrollableText,
|
pub author_name: HorizontallyScrollableText,
|
||||||
pub foreign_author_id: String,
|
pub foreign_author_id: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: AuthorStatus,
|
pub status: AuthorStatus,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
pub author_type: Option<String>,
|
pub author_type: Option<String>,
|
||||||
@@ -38,6 +39,7 @@ pub struct Author {
|
|||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub metadata_profile_id: i64,
|
pub metadata_profile_id: i64,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub monitor_new_items: NewItemMonitorType,
|
pub monitor_new_items: NewItemMonitorType,
|
||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
pub tags: Vec<Number>,
|
pub tags: Vec<Number>,
|
||||||
@@ -152,6 +154,7 @@ pub enum MonitorType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadRecord {
|
pub struct DownloadRecord {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: DownloadStatus,
|
pub status: DownloadStatus,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -169,36 +172,6 @@ pub struct DownloadRecord {
|
|||||||
|
|
||||||
impl Eq for DownloadRecord {}
|
impl Eq for DownloadRecord {}
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
Default,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
Debug,
|
|
||||||
EnumIter,
|
|
||||||
Display,
|
|
||||||
EnumDisplayStyle,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[strum(serialize_all = "camelCase")]
|
|
||||||
pub enum DownloadStatus {
|
|
||||||
#[default]
|
|
||||||
Unknown,
|
|
||||||
Queued,
|
|
||||||
Paused,
|
|
||||||
Downloading,
|
|
||||||
Completed,
|
|
||||||
Failed,
|
|
||||||
Warning,
|
|
||||||
Delay,
|
|
||||||
#[display_style(name = "Download Client Unavailable")]
|
|
||||||
DownloadClientUnavailable,
|
|
||||||
Fallback,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadsResponse {
|
pub struct DownloadsResponse {
|
||||||
@@ -210,6 +183,7 @@ pub struct DownloadsResponse {
|
|||||||
pub struct AddAuthorSearchResult {
|
pub struct AddAuthorSearchResult {
|
||||||
pub foreign_author_id: String,
|
pub foreign_author_id: String,
|
||||||
pub author_name: HorizontallyScrollableText,
|
pub author_name: HorizontallyScrollableText,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: AuthorStatus,
|
pub status: AuthorStatus,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ended: bool,
|
pub ended: bool,
|
||||||
@@ -248,6 +222,8 @@ pub struct AddAuthorBody {
|
|||||||
pub quality_profile_id: i64,
|
pub quality_profile_id: i64,
|
||||||
pub metadata_profile_id: i64,
|
pub metadata_profile_id: i64,
|
||||||
pub tags: Vec<i64>,
|
pub tags: Vec<i64>,
|
||||||
|
// Readarr-Resurrected persists the add body directly and rejects it when the images list is absent
|
||||||
|
pub images: Vec<Value>,
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub tag_input_string: Option<String>,
|
pub tag_input_string: Option<String>,
|
||||||
pub add_options: AddAuthorOptions,
|
pub add_options: AddAuthorOptions,
|
||||||
@@ -306,6 +282,7 @@ pub struct Book {
|
|||||||
pub ratings: Option<Ratings>,
|
pub ratings: Option<Ratings>,
|
||||||
pub release_date: Option<DateTime<Utc>>,
|
pub release_date: Option<DateTime<Utc>>,
|
||||||
pub statistics: Option<BookStatistics>,
|
pub statistics: Option<BookStatistics>,
|
||||||
|
#[serde(default)]
|
||||||
pub grabbed: bool,
|
pub grabbed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,6 +400,7 @@ pub struct ReadarrHistoryItem {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ReadarrTask {
|
pub struct ReadarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub task_name: ReadarrTaskName,
|
pub task_name: ReadarrTaskName,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub interval: i64,
|
pub interval: i64,
|
||||||
@@ -479,6 +457,29 @@ pub struct ReadarrRelease {
|
|||||||
pub quality: QualityWrapper,
|
pub quality: QualityWrapper,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ReadarrReleaseResponse {
|
||||||
|
Releases(Vec<ReadarrRelease>),
|
||||||
|
// Chaptarr wraps the release list in a filter summary envelope, even behind its Readarr facade
|
||||||
|
Envelope { releases: Vec<ReadarrRelease> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ReadarrReleaseResponse {
|
||||||
|
fn default() -> Self {
|
||||||
|
ReadarrReleaseResponse::Releases(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadarrReleaseResponse {
|
||||||
|
pub fn into_releases(self) -> Vec<ReadarrRelease> {
|
||||||
|
match self {
|
||||||
|
ReadarrReleaseResponse::Releases(releases)
|
||||||
|
| ReadarrReleaseResponse::Envelope { releases } => releases,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BlocklistItem {
|
pub struct BlocklistItem {
|
||||||
|
|||||||
@@ -2,23 +2,27 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::readarr_models::{
|
use crate::models::readarr_models::{
|
||||||
AddAuthorSearchResult, BlocklistItem, BlocklistResponse, Book, BookFile, DownloadRecord,
|
AddAuthorSearchResult, BlocklistItem, BlocklistResponse, Book, BookFile, DownloadRecord,
|
||||||
DownloadStatus, DownloadsResponse, Edition, MediaInfo, MonitorType, NewItemMonitorType,
|
DownloadsResponse, Edition, MediaInfo, MonitorType, NewItemMonitorType,
|
||||||
ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrHistoryWrapper, ReadarrRelease,
|
ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrHistoryWrapper, ReadarrRelease,
|
||||||
ReadarrTask, ReadarrTaskName,
|
ReadarrReleaseResponse, ReadarrTask, ReadarrTaskName,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
|
DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log,
|
||||||
MetadataProfile, QualityProfile, QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag,
|
LogResponse, MetadataProfile, QualityProfile, QueueEvent, RootFolder, SecurityConfig,
|
||||||
Update,
|
SystemStatus, Tag, Update,
|
||||||
};
|
};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
Serdeable,
|
Serdeable,
|
||||||
readarr_models::{Author, AuthorStatistics, AuthorStatus, Ratings, ReadarrSerdeable},
|
readarr_models::{Author, AuthorStatistics, AuthorStatus, Ratings, ReadarrSerdeable},
|
||||||
};
|
};
|
||||||
|
use crate::network::readarr_network::readarr_network_test_utils::test_utils::{
|
||||||
|
ADD_AUTHOR_SEARCH_RESULT_JSON, AUTHOR_JSON, download_record,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_author_status_default() {
|
fn test_author_status_default() {
|
||||||
@@ -493,6 +497,29 @@ mod tests {
|
|||||||
assert_eq!(readarr_serdeable, ReadarrSerdeable::Releases(releases));
|
assert_eq!(readarr_serdeable, ReadarrSerdeable::Releases(releases));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readarr_release_response_default_is_an_empty_release_list() {
|
||||||
|
assert_eq!(
|
||||||
|
ReadarrReleaseResponse::default(),
|
||||||
|
ReadarrReleaseResponse::Releases(Vec::new())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readarr_release_response_into_releases() {
|
||||||
|
let releases = vec![ReadarrRelease {
|
||||||
|
guid: "test".to_owned(),
|
||||||
|
..ReadarrRelease::default()
|
||||||
|
}];
|
||||||
|
let list = ReadarrReleaseResponse::Releases(releases.clone());
|
||||||
|
let envelope = ReadarrReleaseResponse::Envelope {
|
||||||
|
releases: releases.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(list.into_releases(), releases);
|
||||||
|
assert_eq!(envelope.into_releases(), releases);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_readarr_serdeable_from_security_config() {
|
fn test_readarr_serdeable_from_security_config() {
|
||||||
let security_config = SecurityConfig {
|
let security_config = SecurityConfig {
|
||||||
@@ -640,40 +667,6 @@ mod tests {
|
|||||||
assert_str_eq!(AuthorStatus::Deleted.to_display_str(), "Deleted");
|
assert_str_eq!(AuthorStatus::Deleted.to_display_str(), "Deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_download_status_display() {
|
|
||||||
assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown");
|
|
||||||
assert_str_eq!(DownloadStatus::Queued.to_string(), "queued");
|
|
||||||
assert_str_eq!(DownloadStatus::Paused.to_string(), "paused");
|
|
||||||
assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading");
|
|
||||||
assert_str_eq!(DownloadStatus::Completed.to_string(), "completed");
|
|
||||||
assert_str_eq!(DownloadStatus::Failed.to_string(), "failed");
|
|
||||||
assert_str_eq!(DownloadStatus::Warning.to_string(), "warning");
|
|
||||||
assert_str_eq!(DownloadStatus::Delay.to_string(), "delay");
|
|
||||||
assert_str_eq!(
|
|
||||||
DownloadStatus::DownloadClientUnavailable.to_string(),
|
|
||||||
"downloadClientUnavailable"
|
|
||||||
);
|
|
||||||
assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_download_status_to_display_str() {
|
|
||||||
assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown");
|
|
||||||
assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued");
|
|
||||||
assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused");
|
|
||||||
assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading");
|
|
||||||
assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed");
|
|
||||||
assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed");
|
|
||||||
assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning");
|
|
||||||
assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay");
|
|
||||||
assert_str_eq!(
|
|
||||||
DownloadStatus::DownloadClientUnavailable.to_display_str(),
|
|
||||||
"Download Client Unavailable"
|
|
||||||
);
|
|
||||||
assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_readarr_history_event_type_display() {
|
fn test_readarr_history_event_type_display() {
|
||||||
assert_str_eq!(ReadarrHistoryEventType::Unknown.to_string(), "unknown");
|
assert_str_eq!(ReadarrHistoryEventType::Unknown.to_string(), "unknown");
|
||||||
@@ -969,6 +962,22 @@ mod tests {
|
|||||||
assert_none!(&book.statistics);
|
assert_none!(&book.statistics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_book_deserialization_defaults_grabbed_when_absent() {
|
||||||
|
let book_json = json!({
|
||||||
|
"id": 1,
|
||||||
|
"title": "The Name of the Wind",
|
||||||
|
"authorId": 3,
|
||||||
|
"foreignBookId": "test-foreign-book-id",
|
||||||
|
"monitored": true
|
||||||
|
});
|
||||||
|
|
||||||
|
let book: Book = serde_json::from_value(book_json).unwrap();
|
||||||
|
|
||||||
|
assert!(!book.grabbed);
|
||||||
|
assert!(book.monitored);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edition_deserialization() {
|
fn test_edition_deserialization() {
|
||||||
let edition_json = json!({
|
let edition_json = json!({
|
||||||
@@ -1140,6 +1149,113 @@ mod tests {
|
|||||||
assert_none!(&search_result.ratings);
|
assert_none!(&search_result.ratings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_author_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Author {
|
||||||
|
status: AuthorStatus::default(),
|
||||||
|
..serde_json::from_str(AUTHOR_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Author>(AUTHOR_JSON, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Author>(AUTHOR_JSON, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_author_monitor_new_items_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Author {
|
||||||
|
monitor_new_items: NewItemMonitorType::default(),
|
||||||
|
..serde_json::from_str(AUTHOR_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Author>(AUTHOR_JSON, "monitorNewItems", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Author>(AUTHOR_JSON, "monitorNewItems", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_author_search_result_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = AddAuthorSearchResult {
|
||||||
|
status: AuthorStatus::default(),
|
||||||
|
..serde_json::from_str(ADD_AUTHOR_SEARCH_RESULT_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddAuthorSearchResult>(
|
||||||
|
ADD_AUTHOR_SEARCH_RESULT_JSON,
|
||||||
|
"status",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddAuthorSearchResult>(
|
||||||
|
ADD_AUTHOR_SEARCH_RESULT_JSON,
|
||||||
|
"status",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_record_status_deserialization_falls_back_to_default() {
|
||||||
|
let download_record_json = serde_json::to_string(&download_record()).unwrap();
|
||||||
|
let expected = DownloadRecord {
|
||||||
|
status: DownloadStatus::default(),
|
||||||
|
..download_record()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readarr_task_task_name_deserialization_falls_back_to_default() {
|
||||||
|
let task = ReadarrTask {
|
||||||
|
name: "Backup".to_owned(),
|
||||||
|
task_name: ReadarrTaskName::Backup,
|
||||||
|
..ReadarrTask::default()
|
||||||
|
};
|
||||||
|
let task_json = serde_json::to_string(&task).unwrap();
|
||||||
|
let expected = ReadarrTask {
|
||||||
|
task_name: ReadarrTaskName::default(),
|
||||||
|
..task
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<ReadarrTask>(&task_json, "taskName", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<ReadarrTask>(&task_json, "taskName", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_history_event_type(event_type: Value) -> ReadarrHistoryEventType {
|
fn deserialize_history_event_type(event_type: Value) -> ReadarrHistoryEventType {
|
||||||
let history_item_json = json!({
|
let history_item_json = json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ use strum::EnumIter;
|
|||||||
use {
|
use {
|
||||||
super::modals::EditionDetailsModal,
|
super::modals::EditionDetailsModal,
|
||||||
crate::models::readarr_models::{
|
crate::models::readarr_models::{
|
||||||
AuthorStatus, BookFile, DownloadStatus, Edition, MediaInfo, MonitorType, NewItemMonitorType,
|
AuthorStatus, BookFile, BookStatistics, Edition, MediaInfo, MonitorType, NewItemMonitorType,
|
||||||
Ratings, ReadarrHistoryEventType, ReadarrTaskName,
|
Ratings, ReadarrHistoryEventType, ReadarrTaskName,
|
||||||
},
|
},
|
||||||
crate::models::servarr_models::{IndexerField, Quality, QualityWrapper},
|
crate::models::servarr_models::{DownloadStatus, IndexerField, Quality, QualityWrapper},
|
||||||
crate::models::stateful_table::SortOption,
|
crate::models::stateful_table::SortOption,
|
||||||
crate::network::servarr_test_utils::{
|
crate::network::servarr_test_utils::{
|
||||||
diskspace, indexer_settings, indexer_test_result, queued_event,
|
diskspace, indexer_settings, indexer_test_result, queued_event,
|
||||||
@@ -292,6 +292,12 @@ impl ReadarrData<'_> {
|
|||||||
any_edition_ok: true,
|
any_edition_ok: true,
|
||||||
page_count: Some(288),
|
page_count: Some(288),
|
||||||
release_date: Some(date),
|
release_date: Some(date),
|
||||||
|
statistics: Some(BookStatistics {
|
||||||
|
book_file_count: 3,
|
||||||
|
total_book_count: 7,
|
||||||
|
size_on_disk: 2457600,
|
||||||
|
percent_of_books: 42.86,
|
||||||
|
}),
|
||||||
..Book::default()
|
..Book::default()
|
||||||
};
|
};
|
||||||
let readarr_history_item = ReadarrHistoryItem {
|
let readarr_history_item = ReadarrHistoryItem {
|
||||||
@@ -532,7 +538,7 @@ impl ReadarrData<'_> {
|
|||||||
author_id: 1,
|
author_id: 1,
|
||||||
book_id: 1,
|
book_id: 1,
|
||||||
path: "/nfs/books/Test Author/Test Book.epub".to_owned(),
|
path: "/nfs/books/Test Author/Test Book.epub".to_owned(),
|
||||||
size: 3543348019,
|
size: 3355443,
|
||||||
date_added: date,
|
date_added: date,
|
||||||
quality: quality.clone(),
|
quality: quality.clone(),
|
||||||
media_info: Some(MediaInfo {
|
media_info: Some(MediaInfo {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ use std::fmt::{Display, Formatter, Result};
|
|||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
|
use enum_display_style_derive::EnumDisplayStyle;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Number, Value};
|
use serde_json::{Number, Value};
|
||||||
|
use strum::{Display, EnumIter};
|
||||||
|
|
||||||
use super::HorizontallyScrollableText;
|
use super::HorizontallyScrollableText;
|
||||||
|
|
||||||
@@ -90,6 +92,36 @@ pub struct DiskSpace {
|
|||||||
pub total_space: i64,
|
pub total_space: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
Default,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
EnumIter,
|
||||||
|
Display,
|
||||||
|
EnumDisplayStyle,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[strum(serialize_all = "camelCase")]
|
||||||
|
pub enum DownloadStatus {
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
Queued,
|
||||||
|
Paused,
|
||||||
|
Downloading,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Warning,
|
||||||
|
Delay,
|
||||||
|
#[display_style(name = "Download Client Unavailable")]
|
||||||
|
DownloadClientUnavailable,
|
||||||
|
Fallback,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct IndexerSettings {
|
pub struct IndexerSettings {
|
||||||
@@ -315,7 +347,9 @@ pub struct RootFolder {
|
|||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SecurityConfig {
|
pub struct SecurityConfig {
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub authentication_method: AuthenticationMethod,
|
pub authentication_method: AuthenticationMethod,
|
||||||
|
#[serde(default, deserialize_with = "super::from_json_or_default")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub authentication_required: Option<AuthenticationRequired>,
|
pub authentication_required: Option<AuthenticationRequired>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -323,6 +357,7 @@ pub struct SecurityConfig {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub certificate_validation: CertificateValidation,
|
pub certificate_validation: CertificateValidation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
AuthenticationMethod, AuthenticationRequired, CertificateValidation, Indexer, QualityProfile,
|
AuthenticationMethod, AuthenticationRequired, CertificateValidation, DownloadStatus, Indexer,
|
||||||
Update, UpdateChanges,
|
QualityProfile, SecurityConfig, Update, UpdateChanges,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -54,6 +56,40 @@ mod tests {
|
|||||||
assert_str_eq!(CertificateValidation::Disabled.to_string(), "disabled");
|
assert_str_eq!(CertificateValidation::Disabled.to_string(), "disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_status_display() {
|
||||||
|
assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown");
|
||||||
|
assert_str_eq!(DownloadStatus::Queued.to_string(), "queued");
|
||||||
|
assert_str_eq!(DownloadStatus::Paused.to_string(), "paused");
|
||||||
|
assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading");
|
||||||
|
assert_str_eq!(DownloadStatus::Completed.to_string(), "completed");
|
||||||
|
assert_str_eq!(DownloadStatus::Failed.to_string(), "failed");
|
||||||
|
assert_str_eq!(DownloadStatus::Warning.to_string(), "warning");
|
||||||
|
assert_str_eq!(DownloadStatus::Delay.to_string(), "delay");
|
||||||
|
assert_str_eq!(
|
||||||
|
DownloadStatus::DownloadClientUnavailable.to_string(),
|
||||||
|
"downloadClientUnavailable"
|
||||||
|
);
|
||||||
|
assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_status_to_display_str() {
|
||||||
|
assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown");
|
||||||
|
assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued");
|
||||||
|
assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused");
|
||||||
|
assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading");
|
||||||
|
assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed");
|
||||||
|
assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed");
|
||||||
|
assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning");
|
||||||
|
assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay");
|
||||||
|
assert_str_eq!(
|
||||||
|
DownloadStatus::DownloadClientUnavailable.to_display_str(),
|
||||||
|
"Download Client Unavailable"
|
||||||
|
);
|
||||||
|
assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_quality_profile_from_tuple_ref() {
|
fn test_quality_profile_from_tuple_ref() {
|
||||||
let id = 2;
|
let id = 2;
|
||||||
@@ -90,4 +126,118 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_config_authentication_method_deserialization_falls_back_to_default() {
|
||||||
|
let security_config_json = serde_json::to_string(&security_config()).unwrap();
|
||||||
|
let expected = SecurityConfig {
|
||||||
|
authentication_method: AuthenticationMethod::default(),
|
||||||
|
..security_config()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"authenticationMethod",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"authenticationMethod",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_config_authentication_required_deserialization_falls_back_to_none() {
|
||||||
|
let security_config_json = serde_json::to_string(&security_config()).unwrap();
|
||||||
|
let expected = SecurityConfig {
|
||||||
|
authentication_required: None,
|
||||||
|
..security_config()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"authenticationRequired",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"authenticationRequired",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_config_deserializes_when_authentication_required_is_absent() {
|
||||||
|
let mut security_config_json = serde_json::to_value(security_config()).unwrap();
|
||||||
|
security_config_json
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.remove("authenticationRequired");
|
||||||
|
let expected = SecurityConfig {
|
||||||
|
authentication_required: None,
|
||||||
|
..security_config()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deserialized = serde_json::from_value::<SecurityConfig>(security_config_json);
|
||||||
|
|
||||||
|
assert_ok!(&deserialized);
|
||||||
|
assert_eq!(deserialized.unwrap(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_config_certificate_validation_deserialization_falls_back_to_default() {
|
||||||
|
let security_config_json = serde_json::to_string(&security_config()).unwrap();
|
||||||
|
let expected = SecurityConfig {
|
||||||
|
certificate_validation: CertificateValidation::default(),
|
||||||
|
..security_config()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"certificateValidation",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"certificateValidation",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn security_config() -> SecurityConfig {
|
||||||
|
SecurityConfig {
|
||||||
|
authentication_method: AuthenticationMethod::Forms,
|
||||||
|
authentication_required: Some(AuthenticationRequired::Enabled),
|
||||||
|
username: Some("admin".to_owned()),
|
||||||
|
password: Some("password".to_owned()),
|
||||||
|
api_key: "test-api-key".to_owned(),
|
||||||
|
certificate_validation: CertificateValidation::Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,8 +16,8 @@ use strum_macros::Display;
|
|||||||
use super::{
|
use super::{
|
||||||
HorizontallyScrollableText, Serdeable,
|
HorizontallyScrollableText, Serdeable,
|
||||||
servarr_models::{
|
servarr_models::{
|
||||||
DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper,
|
DiskSpace, DownloadStatus, HostConfig, Indexer, Language, LogResponse, QualityProfile,
|
||||||
QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag, Update,
|
QualityWrapper, QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag, Update,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,6 +112,7 @@ pub struct DeleteSeriesParams {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadRecord {
|
pub struct DownloadRecord {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: DownloadStatus,
|
pub status: DownloadStatus,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -128,36 +129,6 @@ pub struct DownloadRecord {
|
|||||||
|
|
||||||
impl Eq for DownloadRecord {}
|
impl Eq for DownloadRecord {}
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
Default,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
Debug,
|
|
||||||
EnumIter,
|
|
||||||
Display,
|
|
||||||
EnumDisplayStyle,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[strum(serialize_all = "camelCase")]
|
|
||||||
pub enum DownloadStatus {
|
|
||||||
#[default]
|
|
||||||
Unknown,
|
|
||||||
Queued,
|
|
||||||
Paused,
|
|
||||||
Downloading,
|
|
||||||
Completed,
|
|
||||||
Failed,
|
|
||||||
Warning,
|
|
||||||
Delay,
|
|
||||||
#[display_style(name = "Download Client Unavailable")]
|
|
||||||
DownloadClientUnavailable,
|
|
||||||
Fallback,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadsResponse {
|
pub struct DownloadsResponse {
|
||||||
@@ -314,12 +285,14 @@ pub struct Series {
|
|||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub year: i64,
|
pub year: i64,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub series_type: SeriesType,
|
pub series_type: SeriesType,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
pub tags: Vec<Number>,
|
pub tags: Vec<Number>,
|
||||||
pub ratings: Rating,
|
pub ratings: Rating,
|
||||||
pub ended: bool,
|
pub ended: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: SeriesStatus,
|
pub status: SeriesStatus,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
pub network: Option<String>,
|
pub network: Option<String>,
|
||||||
@@ -556,6 +529,7 @@ pub struct SonarrReleaseDownloadBody {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SonarrTask {
|
pub struct SonarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub task_name: SonarrTaskName,
|
pub task_name: SonarrTaskName,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub interval: i64,
|
pub interval: i64,
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
Serdeable,
|
Serdeable,
|
||||||
servarr_models::{
|
servarr_models::{
|
||||||
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Language, Log,
|
DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Language,
|
||||||
LogResponse, QualityProfile, QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag,
|
Log, LogResponse, QualityProfile, QueueEvent, RootFolder, SecurityConfig, SystemStatus, Tag,
|
||||||
Update,
|
Update,
|
||||||
},
|
},
|
||||||
sonarr_models::{
|
sonarr_models::{
|
||||||
AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadStatus,
|
AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse,
|
||||||
DownloadsResponse, Episode, EpisodeFile, Series, SeriesMonitor, SeriesStatus, SeriesType,
|
Episode, EpisodeFile, Series, SeriesMonitor, SeriesStatus, SeriesType,
|
||||||
SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask,
|
SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask,
|
||||||
SonarrTaskName,
|
SonarrTaskName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
|
||||||
|
SERIES_JSON, download_record, task,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_episode_display() {
|
fn test_episode_display() {
|
||||||
@@ -119,40 +123,6 @@ mod tests {
|
|||||||
assert_str_eq!(SeriesType::Anime.to_display_str(), "Anime");
|
assert_str_eq!(SeriesType::Anime.to_display_str(), "Anime");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_download_status_display() {
|
|
||||||
assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown");
|
|
||||||
assert_str_eq!(DownloadStatus::Queued.to_string(), "queued");
|
|
||||||
assert_str_eq!(DownloadStatus::Paused.to_string(), "paused");
|
|
||||||
assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading");
|
|
||||||
assert_str_eq!(DownloadStatus::Completed.to_string(), "completed");
|
|
||||||
assert_str_eq!(DownloadStatus::Failed.to_string(), "failed");
|
|
||||||
assert_str_eq!(DownloadStatus::Warning.to_string(), "warning");
|
|
||||||
assert_str_eq!(DownloadStatus::Delay.to_string(), "delay");
|
|
||||||
assert_str_eq!(
|
|
||||||
DownloadStatus::DownloadClientUnavailable.to_string(),
|
|
||||||
"downloadClientUnavailable"
|
|
||||||
);
|
|
||||||
assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_download_status_to_display_str() {
|
|
||||||
assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown");
|
|
||||||
assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued");
|
|
||||||
assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused");
|
|
||||||
assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading");
|
|
||||||
assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed");
|
|
||||||
assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed");
|
|
||||||
assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning");
|
|
||||||
assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay");
|
|
||||||
assert_str_eq!(
|
|
||||||
DownloadStatus::DownloadClientUnavailable.to_display_str(),
|
|
||||||
"Download Client Unavailable"
|
|
||||||
);
|
|
||||||
assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sonarr_history_event_type_display() {
|
fn test_sonarr_history_event_type_display() {
|
||||||
assert_str_eq!(SonarrHistoryEventType::Unknown.to_string(), "unknown",);
|
assert_str_eq!(SonarrHistoryEventType::Unknown.to_string(), "unknown",);
|
||||||
@@ -698,6 +668,83 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_series_type_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Series {
|
||||||
|
series_type: SeriesType::default(),
|
||||||
|
..serde_json::from_str(SERIES_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Series>(SERIES_JSON, "seriesType", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Series>(SERIES_JSON, "seriesType", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Series {
|
||||||
|
status: SeriesStatus::default(),
|
||||||
|
..serde_json::from_str(SERIES_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Series>(SERIES_JSON, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Series>(SERIES_JSON, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_record_status_deserialization_falls_back_to_default() {
|
||||||
|
let download_record_json = serde_json::to_string(&download_record()).unwrap();
|
||||||
|
let expected = DownloadRecord {
|
||||||
|
status: DownloadStatus::default(),
|
||||||
|
..download_record()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_task_task_name_deserialization_falls_back_to_default() {
|
||||||
|
let task_json = serde_json::to_string(&task()).unwrap();
|
||||||
|
let expected = SonarrTask {
|
||||||
|
task_name: SonarrTaskName::default(),
|
||||||
|
..task()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SonarrTask>(&task_json, "taskName", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SonarrTask>(&task_json, "taskName", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_history_event_type(event_type: Value) -> SonarrHistoryEventType {
|
fn deserialize_history_event_type(event_type: Value) -> SonarrHistoryEventType {
|
||||||
let history_item_json = json!({
|
let history_item_json = json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -100,29 +100,26 @@ where
|
|||||||
{
|
{
|
||||||
fn page_down(&mut self) {
|
fn page_down(&mut self) {
|
||||||
let items_len = self.active_items().len();
|
let items_len = self.active_items().len();
|
||||||
if items_len <= 1 {
|
if items_len == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = self.active_state_mut();
|
let state = self.active_state_mut();
|
||||||
match state.selected() {
|
match state.selected() {
|
||||||
Some(i) => state.select(Some(i.saturating_add(20) % (items_len - 1))),
|
Some(i) => state.select(Some(i.saturating_add(20).min(items_len - 1))),
|
||||||
None => state.select_first(),
|
None => state.select_first(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn page_up(&mut self) {
|
fn page_up(&mut self) {
|
||||||
let items_len = self.active_items().len();
|
let items_len = self.active_items().len();
|
||||||
if items_len <= 1 {
|
if items_len == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = self.active_state_mut();
|
let state = self.active_state_mut();
|
||||||
match state.selected() {
|
match state.selected() {
|
||||||
Some(i) => {
|
Some(i) => state.select(Some(i.saturating_sub(20))),
|
||||||
let len = items_len - 1;
|
|
||||||
state.select(Some((i + len - (20 % len)) % len));
|
|
||||||
}
|
|
||||||
None => state.select_last(),
|
None => state.select_last(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,69 @@ mod tests {
|
|||||||
20
|
20
|
||||||
);
|
);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
40
|
||||||
|
);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
60
|
||||||
|
);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
80
|
||||||
|
);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
99
|
||||||
|
);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
99
|
||||||
|
);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
79
|
||||||
|
);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
59
|
||||||
|
);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
39
|
||||||
|
);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(
|
||||||
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
|
19
|
||||||
|
);
|
||||||
|
|
||||||
stateful_table.page_up();
|
stateful_table.page_up();
|
||||||
|
|
||||||
assert_some_eq_x!(
|
assert_some_eq_x!(
|
||||||
@@ -327,33 +390,10 @@ mod tests {
|
|||||||
|
|
||||||
stateful_table.page_up();
|
stateful_table.page_up();
|
||||||
|
|
||||||
assert_some_eq_x!(
|
|
||||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
|
||||||
stateful_table.filtered_items.as_ref().unwrap().len() - 21
|
|
||||||
);
|
|
||||||
|
|
||||||
stateful_table.page_down();
|
|
||||||
|
|
||||||
assert_some_eq_x!(
|
assert_some_eq_x!(
|
||||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
stateful_table.scroll_down();
|
|
||||||
stateful_table.page_up();
|
|
||||||
|
|
||||||
assert_some_eq_x!(
|
|
||||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
|
||||||
stateful_table.filtered_items.as_ref().unwrap().len() - 20
|
|
||||||
);
|
|
||||||
|
|
||||||
stateful_table.scroll_down();
|
|
||||||
stateful_table.page_down();
|
|
||||||
|
|
||||||
assert_some_eq_x!(
|
|
||||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
|
||||||
2
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -376,33 +416,105 @@ mod tests {
|
|||||||
|
|
||||||
assert_some_eq_x!(stateful_table.state.selected(), 20);
|
assert_some_eq_x!(stateful_table.state.selected(), 20);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 40);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 60);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 80);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 99);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 99);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 79);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 59);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 39);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 19);
|
||||||
|
|
||||||
stateful_table.page_up();
|
stateful_table.page_up();
|
||||||
|
|
||||||
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
||||||
|
|
||||||
stateful_table.page_up();
|
stateful_table.page_up();
|
||||||
|
|
||||||
assert_some_eq_x!(
|
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
||||||
stateful_table.state.selected(),
|
}
|
||||||
stateful_table.items.len() - 21
|
|
||||||
);
|
|
||||||
|
|
||||||
stateful_table.page_down();
|
#[test]
|
||||||
|
fn test_stateful_table_pagination_on_twenty_one_item_table_stops_at_last_item() {
|
||||||
|
let mut stateful_table = StatefulTable::default();
|
||||||
|
stateful_table.set_items((0..21).collect());
|
||||||
|
|
||||||
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
||||||
|
|
||||||
stateful_table.scroll_down();
|
|
||||||
stateful_table.page_up();
|
|
||||||
|
|
||||||
assert_some_eq_x!(
|
|
||||||
stateful_table.state.selected(),
|
|
||||||
stateful_table.items.len() - 20
|
|
||||||
);
|
|
||||||
|
|
||||||
stateful_table.scroll_down();
|
|
||||||
stateful_table.page_down();
|
stateful_table.page_down();
|
||||||
|
|
||||||
assert_some_eq_x!(stateful_table.state.selected(), 2);
|
assert_some_eq_x!(stateful_table.state.selected(), 20);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 20);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_table_pagination_on_five_item_table_stops_at_last_item() {
|
||||||
|
let mut stateful_table = StatefulTable::default();
|
||||||
|
stateful_table.set_items((0..5).collect());
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 4);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 4);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_table_pagination_on_four_item_table_reaches_last_item() {
|
||||||
|
let mut stateful_table = StatefulTable::default();
|
||||||
|
stateful_table.set_items((0..4).collect());
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
||||||
|
|
||||||
|
stateful_table.page_down();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 3);
|
||||||
|
|
||||||
|
stateful_table.page_up();
|
||||||
|
|
||||||
|
assert_some_eq_x!(stateful_table.state.selected(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ mod tests {
|
|||||||
"trackFileCount": 8,
|
"trackFileCount": 8,
|
||||||
"trackCount": 10,
|
"trackCount": 10,
|
||||||
"totalTrackCount": 10,
|
"totalTrackCount": 10,
|
||||||
"sizeOnDisk": 1024,
|
"sizeOnDisk": 317162578,
|
||||||
"percentOfTracks": 99.9
|
"percentOfTracks": 99.9
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
pub mod test_utils {
|
pub mod test_utils {
|
||||||
use crate::models::lidarr_models::{
|
use crate::models::lidarr_models::{
|
||||||
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
|
AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus,
|
||||||
AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadStatus, DownloadsResponse,
|
AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse,
|
||||||
EditArtistParams, LidarrHistoryData, LidarrHistoryEventType, LidarrHistoryItem,
|
EditArtistParams, LidarrHistoryData, LidarrHistoryEventType, LidarrHistoryItem,
|
||||||
LidarrHistoryWrapper, LidarrRelease, LidarrTask, LidarrTaskName, MediaInfo, Member,
|
LidarrHistoryWrapper, LidarrRelease, LidarrTask, LidarrTaskName, MediaInfo, Member,
|
||||||
NewItemMonitorType, Ratings, Track, TrackFile,
|
NewItemMonitorType, Ratings, Track, TrackFile,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::IndexerSettings;
|
use crate::models::servarr_models::IndexerSettings;
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
Indexer, IndexerField, MetadataProfile, Quality, QualityProfile, QualityWrapper, RootFolder,
|
DownloadStatus, Indexer, IndexerField, MetadataProfile, Quality, QualityProfile,
|
||||||
SystemStatus, Tag,
|
QualityWrapper, RootFolder, SystemStatus, Tag,
|
||||||
};
|
};
|
||||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||||
use bimap::BiMap;
|
use bimap::BiMap;
|
||||||
@@ -74,7 +74,7 @@ pub mod test_utils {
|
|||||||
"trackFileCount": 8,
|
"trackFileCount": 8,
|
||||||
"trackCount": 10,
|
"trackCount": 10,
|
||||||
"totalTrackCount": 10,
|
"totalTrackCount": 10,
|
||||||
"sizeOnDisk": 1024,
|
"sizeOnDisk": 317162578,
|
||||||
"percentOfTracks": 99.9
|
"percentOfTracks": 99.9
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
@@ -184,9 +184,9 @@ pub mod test_utils {
|
|||||||
DownloadRecord {
|
DownloadRecord {
|
||||||
title: "Test download title".to_owned(),
|
title: "Test download title".to_owned(),
|
||||||
status: DownloadStatus::Downloading,
|
status: DownloadStatus::Downloading,
|
||||||
id: 1,
|
id: 9,
|
||||||
album_id: Some(Number::from(1i64)),
|
album_id: Some(Number::from(1i64)),
|
||||||
artist_id: Some(Number::from(1i64)),
|
artist_id: Some(Number::from(3i64)),
|
||||||
size: 3543348019f64,
|
size: 3543348019f64,
|
||||||
sizeleft: 1771674009f64,
|
sizeleft: 1771674009f64,
|
||||||
output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")),
|
output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")),
|
||||||
@@ -250,7 +250,7 @@ pub mod test_utils {
|
|||||||
track_file_count: 8,
|
track_file_count: 8,
|
||||||
track_count: 10,
|
track_count: 10,
|
||||||
total_track_count: 10,
|
total_track_count: 10,
|
||||||
size_on_disk: 1024,
|
size_on_disk: 317162578,
|
||||||
percent_of_tracks: 99.9,
|
percent_of_tracks: 99.9,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,11 +282,11 @@ pub mod test_utils {
|
|||||||
|
|
||||||
pub fn lidarr_history_item() -> LidarrHistoryItem {
|
pub fn lidarr_history_item() -> LidarrHistoryItem {
|
||||||
LidarrHistoryItem {
|
LidarrHistoryItem {
|
||||||
id: 1,
|
id: 13,
|
||||||
source_title: "Test source title".into(),
|
source_title: "Test source title".into(),
|
||||||
album_id: 1,
|
album_id: 1,
|
||||||
artist_id: 1,
|
artist_id: 3,
|
||||||
track_id: 1,
|
track_id: 5,
|
||||||
quality: quality_wrapper(),
|
quality: quality_wrapper(),
|
||||||
date: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
|
date: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
|
||||||
event_type: LidarrHistoryEventType::Grabbed,
|
event_type: LidarrHistoryEventType::Grabbed,
|
||||||
@@ -459,7 +459,7 @@ pub mod test_utils {
|
|||||||
|
|
||||||
pub fn track_file() -> TrackFile {
|
pub fn track_file() -> TrackFile {
|
||||||
TrackFile {
|
TrackFile {
|
||||||
id: 1,
|
id: 7,
|
||||||
path: "/music/P!nk/TRUSTFALL/01 - When I Get There.flac".to_string(),
|
path: "/music/P!nk/TRUSTFALL/01 - When I Get There.flac".to_string(),
|
||||||
size: 39216378,
|
size: 39216378,
|
||||||
quality: quality_wrapper(),
|
quality: quality_wrapper(),
|
||||||
@@ -471,10 +471,10 @@ pub mod test_utils {
|
|||||||
|
|
||||||
pub fn track() -> Track {
|
pub fn track() -> Track {
|
||||||
Track {
|
Track {
|
||||||
id: 1,
|
id: 5,
|
||||||
artist_id: 1,
|
artist_id: 3,
|
||||||
foreign_track_id: "test-foreign-track-id".to_string(),
|
foreign_track_id: "test-foreign-track-id".to_string(),
|
||||||
track_file_id: 1,
|
track_file_id: 7,
|
||||||
album_id: 1,
|
album_id: 1,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
track_number: "1".to_string(),
|
track_number: "1".to_string(),
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ mod tests {
|
|||||||
"tmdbId": 1234,
|
"tmdbId": 1234,
|
||||||
"originalLanguage": {"id": 1, "name": "English"},
|
"originalLanguage": {"id": 1, "name": "English"},
|
||||||
"sizeOnDisk": 3543348019i64,
|
"sizeOnDisk": 3543348019i64,
|
||||||
"status": "Downloaded",
|
"status": "released",
|
||||||
"overview": "Blah blah blah",
|
"overview": "Blah blah blah",
|
||||||
"path": "/nfs/movies",
|
"path": "/nfs/movies",
|
||||||
"studio": "21st Century Alex",
|
"studio": "21st Century Alex",
|
||||||
@@ -127,7 +127,7 @@ mod tests {
|
|||||||
"tmdbId": 1234,
|
"tmdbId": 1234,
|
||||||
"originalLanguage": {"id": 1, "name": "English"},
|
"originalLanguage": {"id": 1, "name": "English"},
|
||||||
"sizeOnDisk": 3543348019i64,
|
"sizeOnDisk": 3543348019i64,
|
||||||
"status": "Downloaded",
|
"status": "released",
|
||||||
"overview": "Blah blah blah",
|
"overview": "Blah blah blah",
|
||||||
"path": "/nfs/movies",
|
"path": "/nfs/movies",
|
||||||
"studio": "21st Century Alex",
|
"studio": "21st Century Alex",
|
||||||
@@ -230,7 +230,7 @@ mod tests {
|
|||||||
"tmdbId": 1234,
|
"tmdbId": 1234,
|
||||||
"originalLanguage": {"id": 1, "name": "English"},
|
"originalLanguage": {"id": 1, "name": "English"},
|
||||||
"sizeOnDisk": 3543348019i64,
|
"sizeOnDisk": 3543348019i64,
|
||||||
"status": "Downloaded",
|
"status": "released",
|
||||||
"overview": "Blah blah blah",
|
"overview": "Blah blah blah",
|
||||||
"path": "/nfs/movies",
|
"path": "/nfs/movies",
|
||||||
"studio": "21st Century Alex",
|
"studio": "21st Century Alex",
|
||||||
@@ -266,7 +266,7 @@ mod tests {
|
|||||||
"tmdbId": 1234,
|
"tmdbId": 1234,
|
||||||
"originalLanguage": {"id": 1, "name": "English"},
|
"originalLanguage": {"id": 1, "name": "English"},
|
||||||
"sizeOnDisk": 3543348019i64,
|
"sizeOnDisk": 3543348019i64,
|
||||||
"status": "Downloaded",
|
"status": "released",
|
||||||
"overview": "Blah blah blah",
|
"overview": "Blah blah blah",
|
||||||
"path": "/nfs/movies",
|
"path": "/nfs/movies",
|
||||||
"studio": "21st Century Alex",
|
"studio": "21st Century Alex",
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ use crate::models::radarr_models::{
|
|||||||
use crate::models::servarr_data::Notification;
|
use crate::models::servarr_data::Notification;
|
||||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
|
use crate::models::servarr_models::DownloadStatus;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use crate::models::{Route, ScrollableText};
|
use crate::models::{Route, ScrollableText};
|
||||||
use crate::network::radarr_network::RadarrEvent;
|
use crate::network::radarr_network::RadarrEvent;
|
||||||
use crate::network::{Network, RequestMethod};
|
use crate::network::{Network, RequestMethod};
|
||||||
use crate::utils::{convert_runtime, convert_to_gb};
|
use crate::utils::{convert_runtime, format_size};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use indoc::formatdoc;
|
use indoc::formatdoc;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
@@ -318,7 +319,7 @@ impl Network<'_, '_> {
|
|||||||
..
|
..
|
||||||
} = movie_response;
|
} = movie_response;
|
||||||
let (hours, minutes) = convert_runtime(runtime);
|
let (hours, minutes) = convert_runtime(runtime);
|
||||||
let size = convert_to_gb(size_on_disk);
|
let size = format_size(size_on_disk, 2);
|
||||||
let studio = studio.clone().unwrap_or_default();
|
let studio = studio.clone().unwrap_or_default();
|
||||||
let quality_profile = app
|
let quality_profile = app
|
||||||
.data
|
.data
|
||||||
@@ -373,7 +374,7 @@ impl Network<'_, '_> {
|
|||||||
IMDB: {imdb_rating}
|
IMDB: {imdb_rating}
|
||||||
Rotten Tomatoes: {rotten_tomatoes_rating}
|
Rotten Tomatoes: {rotten_tomatoes_rating}
|
||||||
Quality Profile: {quality_profile}
|
Quality Profile: {quality_profile}
|
||||||
Size: {size:.2} GB
|
Size: {size}
|
||||||
Path: {path}
|
Path: {path}
|
||||||
Studio: {studio}
|
Studio: {studio}
|
||||||
Genres: {}",
|
Genres: {}",
|
||||||
@@ -392,7 +393,7 @@ impl Network<'_, '_> {
|
|||||||
movie_details_modal.file_details = formatdoc!(
|
movie_details_modal.file_details = formatdoc!(
|
||||||
"Relative Path: {}
|
"Relative Path: {}
|
||||||
Absolute Path: {}
|
Absolute Path: {}
|
||||||
Size: {size:.2} GB
|
Size: {size}
|
||||||
Date Added: {}",
|
Date Added: {}",
|
||||||
file.relative_path,
|
file.relative_path,
|
||||||
file.path,
|
file.path,
|
||||||
@@ -664,11 +665,11 @@ pub(in crate::network::radarr_network::library) fn get_movie_status(
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|&download| download.movie_id == movie_id)
|
.find(|&download| download.movie_id == movie_id)
|
||||||
{
|
{
|
||||||
if download.status == "downloading" {
|
if download.status == DownloadStatus::Downloading {
|
||||||
return "Downloading".to_owned();
|
return "Downloading".to_owned();
|
||||||
}
|
}
|
||||||
|
|
||||||
if download.status == "completed" {
|
if download.status == DownloadStatus::Completed {
|
||||||
return "Awaiting Import".to_owned();
|
return "Awaiting Import".to_owned();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod tests {
|
|||||||
use crate::models::servarr_data::Notification;
|
use crate::models::servarr_data::Notification;
|
||||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
|
use crate::models::servarr_models::DownloadStatus;
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
use crate::network::NetworkResource;
|
use crate::network::NetworkResource;
|
||||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||||
@@ -700,7 +701,7 @@ mod tests {
|
|||||||
"name": "English"
|
"name": "English"
|
||||||
},
|
},
|
||||||
"sizeOnDisk": 0,
|
"sizeOnDisk": 0,
|
||||||
"status": "Downloaded",
|
"status": "released",
|
||||||
"overview": "Blah blah blah",
|
"overview": "Blah blah blah",
|
||||||
"path": "/nfs/movies",
|
"path": "/nfs/movies",
|
||||||
"studio": "21st Century Alex",
|
"studio": "21st Century Alex",
|
||||||
@@ -758,7 +759,7 @@ mod tests {
|
|||||||
IMDB:
|
IMDB:
|
||||||
Rotten Tomatoes:
|
Rotten Tomatoes:
|
||||||
Quality Profile: HD - 1080p
|
Quality Profile: HD - 1080p
|
||||||
Size: 0.00 GB
|
Size: 0.00 MB
|
||||||
Path: /nfs/movies
|
Path: /nfs/movies
|
||||||
Studio: 21st Century Alex
|
Studio: 21st Century Alex
|
||||||
Genres: cool, family, fun"
|
Genres: cool, family, fun"
|
||||||
@@ -1199,7 +1200,7 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
&[DownloadRecord {
|
&[DownloadRecord {
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
status: "downloading".to_owned(),
|
status: DownloadStatus::Downloading,
|
||||||
..DownloadRecord::default()
|
..DownloadRecord::default()
|
||||||
}],
|
}],
|
||||||
1
|
1
|
||||||
@@ -1215,7 +1216,7 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
&[DownloadRecord {
|
&[DownloadRecord {
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
status: "completed".to_owned(),
|
status: DownloadStatus::Completed,
|
||||||
..DownloadRecord::default()
|
..DownloadRecord::default()
|
||||||
}],
|
}],
|
||||||
1
|
1
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ pub mod test_utils {
|
|||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit,
|
AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit,
|
||||||
CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability,
|
CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability,
|
||||||
Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData, RadarrHistoryEventType,
|
Movie, MovieCollection, MovieFile, MovieHistoryItem, MovieStatus, RadarrHistoryData,
|
||||||
RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating, RatingsList,
|
RadarrHistoryEventType, RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating,
|
||||||
|
RatingsList,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
DownloadStatus, Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||||
};
|
};
|
||||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||||
use bimap::BiMap;
|
use bimap::BiMap;
|
||||||
@@ -24,7 +25,7 @@ pub mod test_utils {
|
|||||||
"name": "English"
|
"name": "English"
|
||||||
},
|
},
|
||||||
"sizeOnDisk": 3543348019,
|
"sizeOnDisk": 3543348019,
|
||||||
"status": "Downloaded",
|
"status": "released",
|
||||||
"overview": "Blah blah blah",
|
"overview": "Blah blah blah",
|
||||||
"path": "/nfs/movies",
|
"path": "/nfs/movies",
|
||||||
"studio": "21st Century Alex",
|
"studio": "21st Century Alex",
|
||||||
@@ -233,7 +234,7 @@ pub mod test_utils {
|
|||||||
title: "Test".to_owned().into(),
|
title: "Test".to_owned().into(),
|
||||||
original_language: language(),
|
original_language: language(),
|
||||||
size_on_disk: 3543348019,
|
size_on_disk: 3543348019,
|
||||||
status: "Downloaded".to_owned(),
|
status: MovieStatus::Released,
|
||||||
overview: "Blah blah blah".to_owned(),
|
overview: "Blah blah blah".to_owned(),
|
||||||
path: "/nfs/movies".to_owned(),
|
path: "/nfs/movies".to_owned(),
|
||||||
studio: Some("21st Century Alex".to_owned()),
|
studio: Some("21st Century Alex".to_owned()),
|
||||||
@@ -299,7 +300,7 @@ pub mod test_utils {
|
|||||||
tmdb_id: 1234,
|
tmdb_id: 1234,
|
||||||
title: HorizontallyScrollableText::from("Test"),
|
title: HorizontallyScrollableText::from("Test"),
|
||||||
original_language: language(),
|
original_language: language(),
|
||||||
status: "released".to_owned(),
|
status: MovieStatus::Released,
|
||||||
overview: "New movie blah blah blah".to_owned(),
|
overview: "New movie blah blah blah".to_owned(),
|
||||||
genres: genres(),
|
genres: genres(),
|
||||||
year: 2023,
|
year: 2023,
|
||||||
@@ -339,7 +340,7 @@ pub mod test_utils {
|
|||||||
pub fn download_record() -> DownloadRecord {
|
pub fn download_record() -> DownloadRecord {
|
||||||
DownloadRecord {
|
DownloadRecord {
|
||||||
title: "Test Download Title".to_owned(),
|
title: "Test Download Title".to_owned(),
|
||||||
status: "downloading".to_owned(),
|
status: DownloadStatus::Downloading,
|
||||||
id: 1,
|
id: 1,
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
size: 3543348019,
|
size: 3543348019,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use serde_json::{Value, json};
|
|||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::readarr_models::{
|
use crate::models::readarr_models::{
|
||||||
AddAuthorBody, AddAuthorSearchResult, Author, DeleteParams, EditAuthorParams, ReadarrCommandBody,
|
AddAuthorBody, AddAuthorSearchResult, Author, DeleteParams, EditAuthorParams, ReadarrCommandBody,
|
||||||
ReadarrHistoryItem, ReadarrRelease,
|
ReadarrHistoryItem, ReadarrRelease, ReadarrReleaseResponse,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
@@ -235,10 +235,15 @@ impl Network<'_, '_> {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
self
|
self
|
||||||
.handle_request::<(), Vec<ReadarrRelease>>(request_props, |release_vec, mut app| {
|
.handle_request::<(), ReadarrReleaseResponse>(request_props, |response, mut app| {
|
||||||
app.data.readarr_data.author_releases.set_items(release_vec);
|
app
|
||||||
|
.data
|
||||||
|
.readarr_data
|
||||||
|
.author_releases
|
||||||
|
.set_items(response.into_releases());
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
.map(ReadarrReleaseResponse::into_releases)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::network::readarr_network) async fn get_readarr_author_history(
|
pub(in crate::network::readarr_network) async fn get_readarr_author_history(
|
||||||
|
|||||||
@@ -133,6 +133,48 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_author_releases_event_unwraps_the_release_envelope() {
|
||||||
|
let expected_releases = vec![torrent_release()];
|
||||||
|
let releases_json = json!({
|
||||||
|
"releases": [serde_json::from_str::<Value>(RELEASE_JSON).unwrap()],
|
||||||
|
"hiddenReleases": [],
|
||||||
|
"filterSummary": { "totalResults": 1, "displayedCount": 1 },
|
||||||
|
"siblingToggleEnabled": false
|
||||||
|
});
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(releases_json)
|
||||||
|
.query("authorId=3")
|
||||||
|
.build_for(ReadarrEvent::GetAuthorReleases(3))
|
||||||
|
.await;
|
||||||
|
{
|
||||||
|
let mut app = app.lock().await;
|
||||||
|
app.server_tabs.set_index(3);
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.readarr_data
|
||||||
|
.author_releases
|
||||||
|
.set_items(vec![stale_release()]);
|
||||||
|
}
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_readarr_event(ReadarrEvent::GetAuthorReleases(3))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let ReadarrSerdeable::Releases(releases) = result.unwrap() else {
|
||||||
|
panic!("Expected Releases")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(releases, expected_releases);
|
||||||
|
assert_eq!(
|
||||||
|
app.lock().await.data.readarr_data.author_releases.items,
|
||||||
|
expected_releases
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_handle_get_author_releases_event_failure() {
|
async fn test_handle_get_author_releases_event_failure() {
|
||||||
let stale_releases = vec![stale_release()];
|
let stale_releases = vec![stale_release()];
|
||||||
@@ -638,6 +680,7 @@ mod tests {
|
|||||||
"qualityProfileId": 1,
|
"qualityProfileId": 1,
|
||||||
"metadataProfileId": 1,
|
"metadataProfileId": 1,
|
||||||
"tags": [1, 2],
|
"tags": [1, 2],
|
||||||
|
"images": [],
|
||||||
"addOptions": {
|
"addOptions": {
|
||||||
"monitor": "all",
|
"monitor": "all",
|
||||||
"monitorNewItems": "all",
|
"monitorNewItems": "all",
|
||||||
@@ -680,6 +723,7 @@ mod tests {
|
|||||||
"qualityProfileId": 1,
|
"qualityProfileId": 1,
|
||||||
"metadataProfileId": 1,
|
"metadataProfileId": 1,
|
||||||
"tags": [1, 2],
|
"tags": [1, 2],
|
||||||
|
"images": [],
|
||||||
"addOptions": {
|
"addOptions": {
|
||||||
"monitor": "all",
|
"monitor": "all",
|
||||||
"monitorNewItems": "all",
|
"monitorNewItems": "all",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use serde_json::{Value, json};
|
|||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::readarr_models::{
|
use crate::models::readarr_models::{
|
||||||
Book, BookFile, DeleteParams, Edition, ReadarrCommandBody, ReadarrHistoryItem, ReadarrRelease,
|
Book, BookFile, DeleteParams, Edition, ReadarrCommandBody, ReadarrHistoryItem, ReadarrRelease,
|
||||||
|
ReadarrReleaseResponse,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||||
use crate::network::readarr_network::ReadarrEvent;
|
use crate::network::readarr_network::ReadarrEvent;
|
||||||
@@ -170,16 +171,19 @@ impl Network<'_, '_> {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
self
|
self
|
||||||
.handle_request::<(), Vec<ReadarrRelease>>(request_props, |release_vec, mut app| {
|
.handle_request::<(), ReadarrReleaseResponse>(request_props, |response, mut app| {
|
||||||
let book_details_modal = app
|
let book_details_modal = app
|
||||||
.data
|
.data
|
||||||
.readarr_data
|
.readarr_data
|
||||||
.book_details_modal
|
.book_details_modal
|
||||||
.get_or_insert_default();
|
.get_or_insert_default();
|
||||||
|
|
||||||
book_details_modal.book_releases.set_items(release_vec);
|
book_details_modal
|
||||||
|
.book_releases
|
||||||
|
.set_items(response.into_releases());
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
.map(ReadarrReleaseResponse::into_releases)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::network::readarr_network) async fn get_books(
|
pub(in crate::network::readarr_network) async fn get_books(
|
||||||
|
|||||||
@@ -532,6 +532,57 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_book_releases_event_unwraps_the_release_envelope() {
|
||||||
|
let expected_releases = vec![torrent_release()];
|
||||||
|
let releases_json = json!({
|
||||||
|
"releases": [serde_json::from_str::<Value>(RELEASE_JSON).unwrap()],
|
||||||
|
"hiddenReleases": [],
|
||||||
|
"filterSummary": { "totalResults": 1, "displayedCount": 1 },
|
||||||
|
"siblingToggleEnabled": false
|
||||||
|
});
|
||||||
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
|
.returns(releases_json)
|
||||||
|
.query("bookId=7")
|
||||||
|
.build_for(ReadarrEvent::GetBookReleases(7))
|
||||||
|
.await;
|
||||||
|
{
|
||||||
|
let mut app = app.lock().await;
|
||||||
|
app.server_tabs.set_index(3);
|
||||||
|
let mut book_details_modal = BookDetailsModal::default();
|
||||||
|
book_details_modal
|
||||||
|
.book_releases
|
||||||
|
.set_items(vec![stale_release()]);
|
||||||
|
app.data.readarr_data.book_details_modal = Some(book_details_modal);
|
||||||
|
}
|
||||||
|
let mut network = test_network(&app);
|
||||||
|
|
||||||
|
let result = network
|
||||||
|
.handle_readarr_event(ReadarrEvent::GetBookReleases(7))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock.assert_async().await;
|
||||||
|
|
||||||
|
let ReadarrSerdeable::Releases(releases) = result.unwrap() else {
|
||||||
|
panic!("Expected Releases")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(releases, expected_releases);
|
||||||
|
assert_eq!(
|
||||||
|
app
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.data
|
||||||
|
.readarr_data
|
||||||
|
.book_details_modal
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.book_releases
|
||||||
|
.items,
|
||||||
|
expected_releases
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_handle_get_book_releases_event_empty_book_details_modal() {
|
async fn test_handle_get_book_releases_event_empty_book_details_modal() {
|
||||||
let expected_releases = vec![torrent_release()];
|
let expected_releases = vec![torrent_release()];
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ pub mod test_utils {
|
|||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
use crate::models::readarr_models::{
|
use crate::models::readarr_models::{
|
||||||
AddAuthorBody, AddAuthorOptions, AddAuthorSearchResult, Author, AuthorStatus, BlocklistItem,
|
AddAuthorBody, AddAuthorOptions, AddAuthorSearchResult, Author, AuthorStatus, BlocklistItem,
|
||||||
Book, BookFile, DownloadRecord, DownloadStatus, DownloadsResponse, Edition, MonitorType,
|
Book, BookFile, DownloadRecord, DownloadsResponse, Edition, MonitorType, NewItemMonitorType,
|
||||||
NewItemMonitorType, ReadarrHistoryData, ReadarrHistoryEventType, ReadarrHistoryItem,
|
ReadarrHistoryData, ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrRelease,
|
||||||
ReadarrRelease,
|
|
||||||
};
|
};
|
||||||
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
Indexer, IndexerField, IndexerSettings, Quality, QualityWrapper, RootFolder,
|
DownloadStatus, Indexer, IndexerField, IndexerSettings, Quality, QualityWrapper, RootFolder,
|
||||||
};
|
};
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use serde_json::{Number, json};
|
use serde_json::{Number, json};
|
||||||
@@ -395,6 +394,7 @@ pub mod test_utils {
|
|||||||
quality_profile_id: 1,
|
quality_profile_id: 1,
|
||||||
metadata_profile_id: 1,
|
metadata_profile_id: 1,
|
||||||
tags: Vec::default(),
|
tags: Vec::default(),
|
||||||
|
images: Vec::new(),
|
||||||
tag_input_string: Some("usenet, testing".to_owned()),
|
tag_input_string: Some("usenet, testing".to_owned()),
|
||||||
add_options: AddAuthorOptions {
|
add_options: AddAuthorOptions {
|
||||||
monitor: MonitorType::All,
|
monitor: MonitorType::All,
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ mod tests {
|
|||||||
"records": [{
|
"records": [{
|
||||||
"title": "Test Download Title",
|
"title": "Test Download Title",
|
||||||
"status": "downloading",
|
"status": "downloading",
|
||||||
"id": 1,
|
"id": 9,
|
||||||
"episodeId": 1,
|
"episodeId": 11,
|
||||||
"size": 3543348019f64,
|
"size": 3543348019f64,
|
||||||
"sizeleft": 1771674009f64,
|
"sizeleft": 1771674009f64,
|
||||||
"outputPath": "/nfs/tv/Test show/season 1/",
|
"outputPath": "/nfs/tv/Test show/season 1/",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal;
|
use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||||
use crate::models::servarr_models::Language;
|
use crate::models::servarr_models::{DownloadStatus, Language};
|
||||||
use crate::models::sonarr_models::{
|
use crate::models::sonarr_models::{
|
||||||
DownloadRecord, DownloadStatus, Episode, EpisodeFile, MonitorEpisodeBody, SonarrCommandBody,
|
DownloadRecord, Episode, EpisodeFile, MonitorEpisodeBody, SonarrCommandBody,
|
||||||
SonarrHistoryWrapper, SonarrRelease,
|
SonarrHistoryWrapper, SonarrRelease,
|
||||||
};
|
};
|
||||||
use crate::models::{Route, ScrollableText};
|
use crate::models::{Route, ScrollableText};
|
||||||
use crate::network::sonarr_network::SonarrEvent;
|
use crate::network::sonarr_network::SonarrEvent;
|
||||||
use crate::network::{Network, RequestMethod};
|
use crate::network::{Network, RequestMethod};
|
||||||
use crate::utils::convert_to_gb;
|
use crate::utils::format_size;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use indoc::formatdoc;
|
use indoc::formatdoc;
|
||||||
use log::info;
|
use log::info;
|
||||||
@@ -222,12 +222,12 @@ impl Network<'_, '_> {
|
|||||||
overview.unwrap_or_default(),
|
overview.unwrap_or_default(),
|
||||||
));
|
));
|
||||||
if let Some(file) = episode_file {
|
if let Some(file) = episode_file {
|
||||||
let size = convert_to_gb(file.size);
|
let size = format_size(file.size, 2);
|
||||||
episode_details_modal.file_details = formatdoc!(
|
episode_details_modal.file_details = formatdoc!(
|
||||||
"
|
"
|
||||||
Relative Path: {}
|
Relative Path: {}
|
||||||
Absolute Path: {}
|
Absolute Path: {}
|
||||||
Size: {size:.2} GB
|
Size: {size}
|
||||||
Language: {}
|
Language: {}
|
||||||
Date Added: {}",
|
Date Added: {}",
|
||||||
file.relative_path,
|
file.relative_path,
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::models::servarr_data::sonarr::modals::{EpisodeDetailsModal, SeasonDetailsModal};
|
use crate::models::servarr_data::sonarr::modals::{EpisodeDetailsModal, SeasonDetailsModal};
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||||
|
use crate::models::servarr_models::DownloadStatus;
|
||||||
use crate::models::sonarr_models::{
|
use crate::models::sonarr_models::{
|
||||||
DownloadRecord, DownloadStatus, Episode, MonitorEpisodeBody, Season, Series, SonarrHistoryItem,
|
DownloadRecord, Episode, MonitorEpisodeBody, Season, Series, SonarrHistoryItem,
|
||||||
SonarrHistoryWrapper, SonarrRelease, SonarrSerdeable,
|
SonarrHistoryWrapper, SonarrRelease, SonarrSerdeable,
|
||||||
};
|
};
|
||||||
use crate::models::stateful_table::SortOption;
|
use crate::models::stateful_table::SortOption;
|
||||||
@@ -69,7 +70,7 @@ mod tests {
|
|||||||
..episode()
|
..episode()
|
||||||
};
|
};
|
||||||
let expected_episodes = vec![episode_1.clone(), episode_2.clone(), episode_3.clone()];
|
let expected_episodes = vec![episode_1.clone(), episode_2.clone(), episode_3.clone()];
|
||||||
let mut expected_sorted_episodes = vec![episode_1.clone(), episode_3.clone()];
|
let mut expected_sorted_episodes = vec![episode_3.clone(), episode_1.clone()];
|
||||||
let (mock, app, _server) = MockServarrApi::get()
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
.query("seriesId=1")
|
.query("seriesId=1")
|
||||||
.returns(json!([episode_1, episode_2, episode_3]))
|
.returns(json!([episode_1, episode_2, episode_3]))
|
||||||
@@ -175,6 +176,7 @@ mod tests {
|
|||||||
..episode()
|
..episode()
|
||||||
};
|
};
|
||||||
let expected_episodes = vec![episode_1.clone(), episode_2.clone(), episode_3.clone()];
|
let expected_episodes = vec![episode_1.clone(), episode_2.clone(), episode_3.clone()];
|
||||||
|
let expected_sorted_episodes = vec![episode_2.clone(), episode_3.clone(), episode_1.clone()];
|
||||||
let (mock, app, _server) = MockServarrApi::get()
|
let (mock, app, _server) = MockServarrApi::get()
|
||||||
.query("seriesId=1")
|
.query("seriesId=1")
|
||||||
.returns(json!([episode_1, episode_2, episode_3]))
|
.returns(json!([episode_1, episode_2, episode_3]))
|
||||||
@@ -216,7 +218,7 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.episodes
|
.episodes
|
||||||
.items,
|
.items,
|
||||||
expected_episodes
|
expected_sorted_episodes
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
app
|
app
|
||||||
@@ -294,10 +296,10 @@ mod tests {
|
|||||||
"monitored": true
|
"monitored": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 11,
|
||||||
"seriesId": 1,
|
"seriesId": 1,
|
||||||
"tvdbId": 1234,
|
"tvdbId": 1234,
|
||||||
"episodeFileId": 1,
|
"episodeFileId": 7,
|
||||||
"seasonNumber": 1,
|
"seasonNumber": 1,
|
||||||
"episodeNumber": 1,
|
"episodeNumber": 1,
|
||||||
"title": "Something cool",
|
"title": "Something cool",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test_utils {
|
pub mod test_utils {
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
DownloadStatus, Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||||
};
|
};
|
||||||
use crate::models::sonarr_models::{
|
use crate::models::sonarr_models::{
|
||||||
AddSeriesSearchResult, AddSeriesSearchResultStatistics, BlocklistItem, DownloadRecord,
|
AddSeriesSearchResult, AddSeriesSearchResultStatistics, BlocklistItem, DownloadRecord,
|
||||||
DownloadStatus, DownloadsResponse, Episode, EpisodeFile, MediaInfo, Rating, Season,
|
DownloadsResponse, Episode, EpisodeFile, MediaInfo, Rating, Season, SeasonStatistics, Series,
|
||||||
SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, SonarrHistoryData,
|
SeriesStatistics, SeriesStatus, SeriesType, SonarrHistoryData, SonarrHistoryEventType,
|
||||||
SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrTask, SonarrTaskName,
|
SonarrHistoryItem, SonarrRelease, SonarrTask, SonarrTaskName,
|
||||||
};
|
};
|
||||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||||
use bimap::BiMap;
|
use bimap::BiMap;
|
||||||
@@ -64,14 +64,14 @@ pub mod test_utils {
|
|||||||
pub const EPISODE_JSON: &str = r#"{
|
pub const EPISODE_JSON: &str = r#"{
|
||||||
"seriesId": 1,
|
"seriesId": 1,
|
||||||
"tvdbId": 1234,
|
"tvdbId": 1234,
|
||||||
"episodeFileId": 1,
|
"episodeFileId": 7,
|
||||||
"seasonNumber": 1,
|
"seasonNumber": 1,
|
||||||
"episodeNumber": 1,
|
"episodeNumber": 1,
|
||||||
"title": "Something cool",
|
"title": "Something cool",
|
||||||
"airDateUtc": "2024-02-10T07:28:45Z",
|
"airDateUtc": "2024-02-10T07:28:45Z",
|
||||||
"overview": "Okay so this one time at band camp...",
|
"overview": "Okay so this one time at band camp...",
|
||||||
"episodeFile": {
|
"episodeFile": {
|
||||||
"id": 1,
|
"id": 7,
|
||||||
"relativePath": "/season 1/episode 1.mkv",
|
"relativePath": "/season 1/episode 1.mkv",
|
||||||
"path": "/nfs/tv/series/season 1/episode 1.mkv",
|
"path": "/nfs/tv/series/season 1/episode 1.mkv",
|
||||||
"size": 3543348019,
|
"size": 3543348019,
|
||||||
@@ -96,7 +96,7 @@ pub mod test_utils {
|
|||||||
},
|
},
|
||||||
"hasFile": true,
|
"hasFile": true,
|
||||||
"monitored": true,
|
"monitored": true,
|
||||||
"id": 1
|
"id": 11
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
pub fn add_series_search_result() -> AddSeriesSearchResult {
|
pub fn add_series_search_result() -> AddSeriesSearchResult {
|
||||||
@@ -139,8 +139,8 @@ pub mod test_utils {
|
|||||||
DownloadRecord {
|
DownloadRecord {
|
||||||
title: "Test Download Title".to_owned(),
|
title: "Test Download Title".to_owned(),
|
||||||
status: DownloadStatus::Downloading,
|
status: DownloadStatus::Downloading,
|
||||||
id: 1,
|
id: 9,
|
||||||
episode_id: Some(Number::from(1i64)),
|
episode_id: Some(Number::from(11i64)),
|
||||||
size: 3543348019f64,
|
size: 3543348019f64,
|
||||||
sizeleft: 1771674009f64,
|
sizeleft: 1771674009f64,
|
||||||
output_path: Some(HorizontallyScrollableText::from(
|
output_path: Some(HorizontallyScrollableText::from(
|
||||||
@@ -159,10 +159,10 @@ pub mod test_utils {
|
|||||||
|
|
||||||
pub fn episode() -> Episode {
|
pub fn episode() -> Episode {
|
||||||
Episode {
|
Episode {
|
||||||
id: 1,
|
id: 11,
|
||||||
series_id: 1,
|
series_id: 1,
|
||||||
tvdb_id: 1234,
|
tvdb_id: 1234,
|
||||||
episode_file_id: 1,
|
episode_file_id: 7,
|
||||||
season_number: 1,
|
season_number: 1,
|
||||||
episode_number: 1,
|
episode_number: 1,
|
||||||
title: "Something cool".to_owned(),
|
title: "Something cool".to_owned(),
|
||||||
@@ -178,7 +178,7 @@ pub mod test_utils {
|
|||||||
|
|
||||||
pub fn episode_file() -> EpisodeFile {
|
pub fn episode_file() -> EpisodeFile {
|
||||||
EpisodeFile {
|
EpisodeFile {
|
||||||
id: 1,
|
id: 7,
|
||||||
relative_path: "/season 1/episode 1.mkv".to_owned(),
|
relative_path: "/season 1/episode 1.mkv".to_owned(),
|
||||||
path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(),
|
path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(),
|
||||||
size: 3543348019,
|
size: 3543348019,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
|
|||||||
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
|
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
|
||||||
use crate::ui::widgets::managarr_table::ManagarrTable;
|
use crate::ui::widgets::managarr_table::ManagarrTable;
|
||||||
use crate::ui::widgets::popup::{Popup, Size};
|
use crate::ui::widgets::popup::{Popup, Size};
|
||||||
use crate::utils::convert_f64_to_gb;
|
use crate::utils::format_size;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "downloads_ui_tests.rs"]
|
#[path = "downloads_ui_tests.rs"]
|
||||||
@@ -97,12 +97,12 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
} else {
|
} else {
|
||||||
1f64 - (*sizeleft / *size)
|
1f64 - (*sizeleft / *size)
|
||||||
};
|
};
|
||||||
let file_size: f64 = convert_f64_to_gb(*size);
|
let file_size = format_size(*size as i64, 2);
|
||||||
|
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(title.to_owned()),
|
Cell::from(title.to_owned()),
|
||||||
Cell::from(format!("{:.0}%", percent * 100.0)),
|
Cell::from(format!("{:.0}%", percent * 100.0)),
|
||||||
Cell::from(format!("{file_size:.2} GB")),
|
Cell::from(file_size),
|
||||||
Cell::from(
|
Cell::from(
|
||||||
output_path
|
output_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track};
|
use crate::models::lidarr_models::{DownloadRecord, LidarrHistoryItem, LidarrRelease, Track};
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
|
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
|
||||||
|
use crate::models::servarr_models::DownloadStatus;
|
||||||
use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi;
|
use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi;
|
||||||
use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details;
|
use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details;
|
||||||
use crate::ui::styles::{ManagarrStyle, secondary_style};
|
use crate::ui::styles::{ManagarrStyle, secondary_style};
|
||||||
@@ -14,7 +15,7 @@ use crate::ui::widgets::managarr_table::ManagarrTable;
|
|||||||
use crate::ui::widgets::message::Message;
|
use crate::ui::widgets::message::Message;
|
||||||
use crate::ui::widgets::popup::{Popup, Size};
|
use crate::ui::widgets::popup::{Popup, Size};
|
||||||
use crate::ui::{DrawUi, draw_popup, draw_tabs};
|
use crate::ui::{DrawUi, draw_popup, draw_tabs};
|
||||||
use crate::utils::convert_to_gb;
|
use crate::utils::format_size;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use ratatui::layout::{Alignment, Constraint, Rect};
|
use ratatui::layout::{Alignment, Constraint, Rect};
|
||||||
use ratatui::prelude::{Line, Stylize, Text};
|
use ratatui::prelude::{Line, Stylize, Text};
|
||||||
@@ -150,6 +151,7 @@ fn draw_tracks_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.expect("Album details modal is unpopulated")
|
.expect("Album details modal is unpopulated")
|
||||||
.tracks,
|
.tracks,
|
||||||
);
|
);
|
||||||
|
let downloads_vec = &app.data.lidarr_data.downloads.items;
|
||||||
|
|
||||||
let track_row_mapping = |track: &Track| {
|
let track_row_mapping = |track: &Track| {
|
||||||
let Track {
|
let Track {
|
||||||
@@ -187,6 +189,7 @@ fn draw_tracks_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
let duration_str = format!("{mins}:{secs:02}");
|
let duration_str = format!("{mins}:{secs:02}");
|
||||||
|
|
||||||
decorate_track_row_with_style(
|
decorate_track_row_with_style(
|
||||||
|
downloads_vec,
|
||||||
track,
|
track,
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(track_number.clone()),
|
Cell::from(track_number.clone()),
|
||||||
@@ -221,12 +224,35 @@ fn draw_tracks_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decorate_track_row_with_style<'a>(track: &Track, row: Row<'a>) -> Row<'a> {
|
fn decorate_track_row_with_style<'a>(
|
||||||
if track.has_file {
|
downloads_vec: &[DownloadRecord],
|
||||||
row.downloaded()
|
track: &Track,
|
||||||
} else {
|
row: Row<'a>,
|
||||||
row.missing()
|
) -> Row<'a> {
|
||||||
|
if !track.has_file {
|
||||||
|
let default_album_id = Number::from(-1i64);
|
||||||
|
if let Some(download) = downloads_vec.iter().find(|&download| {
|
||||||
|
download
|
||||||
|
.album_id
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&default_album_id)
|
||||||
|
.as_i64()
|
||||||
|
.unwrap()
|
||||||
|
== track.album_id
|
||||||
|
}) {
|
||||||
|
if download.status == DownloadStatus::Downloading {
|
||||||
|
return row.downloading();
|
||||||
|
}
|
||||||
|
|
||||||
|
if download.status == DownloadStatus::Completed {
|
||||||
|
return row.awaiting_import();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return row.missing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row.downloaded()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_album_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
fn draw_album_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||||
@@ -351,7 +377,7 @@ fn draw_album_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
&& active_lidarr_block != ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt,
|
&& active_lidarr_block != ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt,
|
||||||
app.should_text_scroll,
|
app.should_text_scroll,
|
||||||
);
|
);
|
||||||
let size = convert_to_gb(*size);
|
let size = format_size(*size, 1);
|
||||||
let rejected_str = if *rejected { "⛔" } else { "" };
|
let rejected_str = if *rejected { "⛔" } else { "" };
|
||||||
let peers = if seeders.is_none() || leechers.is_none() {
|
let peers = if seeders.is_none() || leechers.is_none() {
|
||||||
Text::from("")
|
Text::from("")
|
||||||
@@ -382,7 +408,7 @@ fn draw_album_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(rejected_str),
|
Cell::from(rejected_str),
|
||||||
Cell::from(title.to_string()),
|
Cell::from(title.to_string()),
|
||||||
Cell::from(indexer.clone()),
|
Cell::from(indexer.clone()),
|
||||||
Cell::from(format!("{size:.1} GB")),
|
Cell::from(size),
|
||||||
Cell::from(peers),
|
Cell::from(peers),
|
||||||
Cell::from(quality_name),
|
Cell::from(quality_name),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
use serde_json::Number;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::lidarr_models::Track;
|
use crate::models::lidarr_models::{DownloadRecord, Track};
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||||
ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock, TRACK_DETAILS_BLOCKS,
|
ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock, TRACK_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::servarr_models::DownloadStatus;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
|
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
|
||||||
use crate::ui::DrawUi;
|
use crate::ui::DrawUi;
|
||||||
use crate::ui::lidarr_ui::library::album_details_ui::{
|
use crate::ui::lidarr_ui::library::album_details_ui::{
|
||||||
AlbumDetailsUi, decorate_track_row_with_style,
|
AlbumDetailsUi, decorate_track_row_with_style,
|
||||||
@@ -39,7 +42,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
let style = decorate_track_row_with_style(&track, row.clone());
|
let style = decorate_track_row_with_style(&[], &track, row.clone());
|
||||||
|
|
||||||
assert_eq!(style, row.downloaded());
|
assert_eq!(style, row.downloaded());
|
||||||
}
|
}
|
||||||
@@ -52,11 +55,163 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
let style = decorate_track_row_with_style(&track, row.clone());
|
let style = decorate_track_row_with_style(&[], &track, row.clone());
|
||||||
|
|
||||||
assert_eq!(style, row.missing());
|
assert_eq!(style, row.missing());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_downloading_when_track_has_no_file_and_album_is_downloading()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![album_download(7, DownloadStatus::Downloading)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.downloading());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_awaiting_import_when_track_has_no_file_and_album_download_is_completed()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![album_download(7, DownloadStatus::Completed)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.awaiting_import());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_missing_when_album_download_is_neither_downloading_nor_completed()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![album_download(7, DownloadStatus::Queued)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_missing_when_only_a_different_album_is_downloading() {
|
||||||
|
let downloads_vec = vec![album_download(8, DownloadStatus::Downloading)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_downloaded_when_track_has_file_and_album_is_downloading() {
|
||||||
|
let downloads_vec = vec![album_download(7, DownloadStatus::Downloading)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: true,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.downloaded());
|
||||||
|
}
|
||||||
|
|
||||||
|
mod test_track_row_styling {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use ratatui::style::Style;
|
||||||
|
|
||||||
|
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::track;
|
||||||
|
use crate::ui::styles::{downloading_style, missing_style};
|
||||||
|
use crate::ui::ui_test_utils::test_utils::{TerminalSize, create_test_terminal};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_album_details_ui_renders_downloading_track_with_downloading_style() {
|
||||||
|
let mut app = App::test_default_fully_populated();
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||||
|
set_tracks_with_unselected_probe(&mut app);
|
||||||
|
|
||||||
|
let style = rendered_row_style(&mut app, "Unselected track");
|
||||||
|
|
||||||
|
assert_eq!(style.fg, downloading_style().fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_album_details_ui_renders_missing_track_with_missing_style_when_queue_is_empty() {
|
||||||
|
let mut app = App::test_default_fully_populated();
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||||
|
app.data.lidarr_data.downloads.set_items(vec![]);
|
||||||
|
set_tracks_with_unselected_probe(&mut app);
|
||||||
|
|
||||||
|
let style = rendered_row_style(&mut app, "Unselected track");
|
||||||
|
|
||||||
|
assert_eq!(style.fg, missing_style().fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_tracks_with_unselected_probe(app: &mut App<'_>) {
|
||||||
|
let album_details_modal = app.data.lidarr_data.album_details_modal.as_mut().unwrap();
|
||||||
|
album_details_modal.album_details_tabs.set_index(0);
|
||||||
|
album_details_modal.tracks.set_items(vec![
|
||||||
|
track(),
|
||||||
|
Track {
|
||||||
|
has_file: false,
|
||||||
|
title: "Unselected track".to_owned(),
|
||||||
|
..track()
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered_row_style(app: &mut App<'_>, needle: &str) -> Style {
|
||||||
|
let (width, height) = TerminalSize::Large.to_cartesian();
|
||||||
|
let mut terminal = create_test_terminal(width, height);
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.draw(|f| {
|
||||||
|
AlbumDetailsUi::draw(f, app, f.area());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
|
||||||
|
for y in 0..height {
|
||||||
|
let row = (0..width)
|
||||||
|
.map(|x| buffer.cell((x, y)).expect("a rendered cell").symbol())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
if let Some(byte_index) = row.find(needle) {
|
||||||
|
let column = row[..byte_index].chars().count() as u16;
|
||||||
|
|
||||||
|
return buffer.cell((column, y)).expect("a rendered cell").style();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("no rendered row contained {needle}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod snapshot_tests {
|
mod snapshot_tests {
|
||||||
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
@@ -177,4 +332,13 @@ mod tests {
|
|||||||
insta::assert_snapshot!(output);
|
insta::assert_snapshot!(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn album_download(album_id: i64, status: DownloadStatus) -> DownloadRecord {
|
||||||
|
DownloadRecord {
|
||||||
|
status,
|
||||||
|
album_id: Some(Number::from(album_id)),
|
||||||
|
artist_id: Some(Number::from(3i64)),
|
||||||
|
..download_record()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ use crate::ui::widgets::managarr_table::ManagarrTable;
|
|||||||
use crate::ui::widgets::message::Message;
|
use crate::ui::widgets::message::Message;
|
||||||
use crate::ui::widgets::popup::{Popup, Size};
|
use crate::ui::widgets::popup::{Popup, Size};
|
||||||
use crate::ui::{DrawUi, draw_popup, draw_tabs};
|
use crate::ui::{DrawUi, draw_popup, draw_tabs};
|
||||||
use crate::utils::convert_to_gb;
|
use crate::utils::format_size;
|
||||||
use ratatui::layout::Alignment;
|
use ratatui::layout::Alignment;
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use serde_json::Number;
|
use serde_json::Number;
|
||||||
@@ -223,7 +223,7 @@ fn draw_artist_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if let Some(stats) = current_selection.statistics.as_ref() {
|
if let Some(stats) = current_selection.statistics.as_ref() {
|
||||||
let size = convert_to_gb(stats.size_on_disk);
|
let size = format_size(stats.size_on_disk, 2);
|
||||||
artist_description.extend(vec![
|
artist_description.extend(vec![
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
"Albums: ".primary().bold(),
|
"Albums: ".primary().bold(),
|
||||||
@@ -235,7 +235,7 @@ fn draw_artist_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
"Size on Disk: ".primary().bold(),
|
"Size on Disk: ".primary().bold(),
|
||||||
format!("{size:.2} GB").default_color(),
|
size.default_color(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -279,13 +279,13 @@ fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.release_date
|
.release_date
|
||||||
.map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string());
|
.map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string());
|
||||||
let track_count = album.statistics.as_ref().map_or_else(
|
let track_count = album.statistics.as_ref().map_or_else(
|
||||||
|| "0/0".to_owned(),
|
|| "N/A".to_owned(),
|
||||||
|s| format!("{}/{}", s.track_file_count, s.total_track_count),
|
|s| format!("{}/{}", s.track_file_count, s.total_track_count),
|
||||||
);
|
);
|
||||||
let size = album
|
let size = album
|
||||||
.statistics
|
.statistics
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0f64, |s| convert_to_gb(s.size_on_disk));
|
.map_or_else(|| "N/A".to_owned(), |s| format_size(s.size_on_disk, 2));
|
||||||
let duration_mins = album.duration / 60000;
|
let duration_mins = album.duration / 60000;
|
||||||
|
|
||||||
decorate_album_row_with_style(
|
decorate_album_row_with_style(
|
||||||
@@ -297,7 +297,7 @@ fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(track_count),
|
Cell::from(track_count),
|
||||||
Cell::from(format!("{duration_mins} min")),
|
Cell::from(format!("{duration_mins} min")),
|
||||||
Cell::from(release_date),
|
Cell::from(release_date),
|
||||||
Cell::from(format!("{size:.2} GB")),
|
Cell::from(size),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -497,7 +497,7 @@ fn draw_artist_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
&& active_lidarr_block != ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
|
&& active_lidarr_block != ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
|
||||||
app.should_text_scroll,
|
app.should_text_scroll,
|
||||||
);
|
);
|
||||||
let size = convert_to_gb(*size);
|
let size = format_size(*size, 1);
|
||||||
let rejected_str = if *rejected { "⛔" } else { "" };
|
let rejected_str = if *rejected { "⛔" } else { "" };
|
||||||
let peers = if seeders.is_none() || leechers.is_none() {
|
let peers = if seeders.is_none() || leechers.is_none() {
|
||||||
Text::from("")
|
Text::from("")
|
||||||
@@ -528,7 +528,7 @@ fn draw_artist_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(rejected_str),
|
Cell::from(rejected_str),
|
||||||
Cell::from(title.to_string()),
|
Cell::from(title.to_string()),
|
||||||
Cell::from(indexer.clone()),
|
Cell::from(indexer.clone()),
|
||||||
Cell::from(format!("{size:.1} GB")),
|
Cell::from(size),
|
||||||
Cell::from(peers),
|
Cell::from(peers),
|
||||||
Cell::from(quality_name),
|
Cell::from(quality_name),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -249,6 +249,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_artist_details_ui_renders_album_without_statistics() {
|
||||||
|
let mut app = App::test_default_fully_populated();
|
||||||
|
app.data.lidarr_data.albums.items[0].statistics = None;
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||||
|
|
||||||
|
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||||
|
ArtistDetailsUi::draw(f, app, f.area());
|
||||||
|
});
|
||||||
|
|
||||||
|
insta::assert_snapshot!(output);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_artist_details_ui_renders_delete_album_prompt_over_artist_details() {
|
fn test_artist_details_ui_renders_delete_album_prompt_over_artist_details() {
|
||||||
let mut app = App::test_default_fully_populated();
|
let mut app = App::test_default_fully_populated();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::ui::widgets::{
|
|||||||
confirmation_prompt::ConfirmationPrompt,
|
confirmation_prompt::ConfirmationPrompt,
|
||||||
popup::{Popup, Size},
|
popup::{Popup, Size},
|
||||||
};
|
};
|
||||||
use crate::utils::convert_to_gb;
|
use crate::utils::format_size;
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
models::{
|
models::{
|
||||||
@@ -104,7 +104,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
let size = artist
|
let size = artist
|
||||||
.statistics
|
.statistics
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0f64, |stats| convert_to_gb(stats.size_on_disk));
|
.map_or(0, |stats| stats.size_on_disk);
|
||||||
let quality_profile = quality_profile_map
|
let quality_profile = quality_profile_map
|
||||||
.get_by_left(&artist.quality_profile_id)
|
.get_by_left(&artist.quality_profile_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -140,7 +140,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(metadata_profile),
|
Cell::from(metadata_profile),
|
||||||
Cell::from(albums.to_string()),
|
Cell::from(albums.to_string()),
|
||||||
Cell::from(tracks),
|
Cell::from(tracks),
|
||||||
Cell::from(format!("{size:.2} GB")),
|
Cell::from(format_size(size, 2)),
|
||||||
Cell::from(monitored.to_owned()),
|
Cell::from(monitored.to_owned()),
|
||||||
Cell::from(tags),
|
Cell::from(tags),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ expression: output
|
|||||||
│ Tracks │ History │ Manual Search │
|
│ Tracks │ History │ Manual Search │
|
||||||
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||||
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
|
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
|
||||||
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │
|
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless │
|
||||||
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │
|
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ expression: output
|
|||||||
│ Tracks │ History │ Manual Search │
|
│ Tracks │ History │ Manual Search │
|
||||||
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||||
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
|
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
|
||||||
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │
|
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless │
|
||||||
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │
|
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ expression: output
|
|||||||
│ Tracks │ History │ Manual Search │
|
│ Tracks │ History │ Manual Search │
|
||||||
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||||
│ Source Age ⛔ Title Indexer Size Peers Quality │
|
│ Source Age ⛔ Title Indexer Size Peers Quality │
|
||||||
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │
|
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless │
|
||||||
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │
|
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ expression: output
|
|||||||
│ Tracks │ History │ Manual Search │
|
│ Tracks │ History │ Manual Search │
|
||||||
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||||
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
|
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
|
||||||
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │
|
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless │
|
||||||
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │
|
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search ││
|
││ Albums │ History │ Manual Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Monitored Title Type Tracks Duration Release Date Size ││
|
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||||
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 302.47 MB ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search │ │ ││
|
││ Albums │ History │ Manual Search │ │ ││
|
||||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │ Release Date Size ││
|
││ Monitored Title │ │ Release Date Size ││
|
||||||
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album │ │ 2023-01-01 302.47 MB ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search │ │ ││
|
││ Albums │ History │ Manual Search │ │ ││
|
||||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||||
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
|
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
|
||||||
││=> torrent 1 days ⛔ Test Releas│ │ 0.0 GB 2 / 1 Lossless ││
|
││=> torrent 1 days ⛔ Test Releas│ │ 0.0 MB 2 / 1 Lossless ││
|
||||||
││ usenet 1 days ⛔ Test Releas│ │ 0.0 GB Lossless ││
|
││ usenet 1 days ⛔ Test Releas│ │ 0.0 MB Lossless ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search │ │ ││
|
││ Albums │ History │ Manual Search │ │ ││
|
||||||
││────────────────────────────────────────│Rejection reasons: │───────────────────────────────────────││
|
││────────────────────────────────────────│Rejection reasons: │───────────────────────────────────────││
|
||||||
││ Source ▼ Age ⛔ Title │• Unknown quality profile │e Peers Quality ││
|
││ Source ▼ Age ⛔ Title │• Unknown quality profile │e Peers Quality ││
|
||||||
││=> torrent 1 days ⛔ Test Relea│• Release is already mapped │ GB 2 / 1 Lossless ││
|
││=> torrent 1 days ⛔ Test Relea│• Release is already mapped │ MB 2 / 1 Lossless ││
|
||||||
││ usenet 1 days ⛔ Test Relea│ │ GB Lossless ││
|
││ usenet 1 days ⛔ Test Relea│ │ MB Lossless ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search ││
|
││ Albums │ History │ Manual Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Source Age ⛔ Title Indexer Size Peers Quality ││
|
││ Source Age ⛔ Title Indexer Size Peers Quality ││
|
||||||
││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless ││
|
││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless ││
|
||||||
││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless ││
|
││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search ││
|
││ Albums │ History │ Manual Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Source ▼ Age ⛔ Title Indexer Size Peers Quality ││
|
││ Source ▼ Age ⛔ Title Indexer Size Peers Quality ││
|
||||||
││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless ││
|
││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless ││
|
||||||
││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless ││
|
││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search ││
|
││ Albums │ History │ Manual Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Monitored Title Type Tracks Duration Release Date Size ││
|
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||||
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 302.47 MB ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search ││
|
││ Albums │ History │ Manual Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Monitored Title Type Tracks Duration Release Date Size ││
|
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||||
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 302.47 MB ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search │ │ ││
|
││ Albums │ History │ Manual Search │ │ ││
|
||||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │ Release Date Size ││
|
││ Monitored Title │ │ Release Date Size ││
|
||||||
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album │ │ 2023-01-01 302.47 MB ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search │ │ ││
|
││ Albums │ History │ Manual Search │ │ ││
|
||||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||||
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
|
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
|
||||||
││=> torrent 1 days ⛔ Test Releas│ │ 0.0 GB 2 / 1 Lossless ││
|
││=> torrent 1 days ⛔ Test Releas│ │ 0.0 MB 2 / 1 Lossless ││
|
||||||
││ usenet 1 days ⛔ Test Releas│ │ 0.0 GB Lossless ││
|
││ usenet 1 days ⛔ Test Releas│ │ 0.0 MB Lossless ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│Artist: Alex │
|
||||||
|
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
||||||
|
│seven years to finish. │
|
||||||
|
│Disambiguation: American pianist │
|
||||||
|
│Type: Person │
|
||||||
|
│Status: Continuing │
|
||||||
|
│Genres: soundtrack │
|
||||||
|
│Rating: 84% │
|
||||||
|
│Path: /nfs/music/test-artist │
|
||||||
|
│Quality Profile: Lossless │
|
||||||
|
│Metadata Profile: Standard │
|
||||||
|
│Monitored: Yes │
|
||||||
|
│Albums: 1 │
|
||||||
|
│Tracks: 12/15 │
|
||||||
|
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||||
|
││ Albums │ History │ Manual Search ││
|
||||||
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
|
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||||
|
││=> 🏷 Test Album Album N/A 0 min 2023-01-01 N/A ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||||
|
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search │ │ ││
|
││ Albums │ History │ Manual Search │ │ ││
|
||||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │ Release Date Size ││
|
││ Monitored Title │ │ Release Date Size ││
|
||||||
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album │ │ 2023-01-01 302.47 MB ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search │ │ ││
|
││ Albums │ History │ Manual Search │ │ ││
|
||||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ ╭───╮ │ Release Date Size ││
|
││ Monitored Title │ ╭───╮ │ Release Date Size ││
|
||||||
││=> 🏷 Test Album │ Delete Album Files: │ ✔ │ │ 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album │ Delete Album Files: │ ✔ │ │ 2023-01-01 302.47 MB ││
|
||||||
││ │ ╰───╯ │ ││
|
││ │ ╰───╯ │ ││
|
||||||
││ │ ╭───╮ │ ││
|
││ │ ╭───╮ │ ││
|
||||||
││ │ Add List Exclusion: │ │ │ ││
|
││ │ Add List Exclusion: │ │ │ ││
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search │ │ ││
|
││ Albums │ History │ Manual Search │ │ ││
|
||||||
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │ Release Date Size ││
|
││ Monitored Title │ │ Release Date Size ││
|
||||||
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album │ │ 2023-01-01 302.47 MB ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Artist: Alex │
|
│Artist: Alex │
|
||||||
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││
|
││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││
|
||||||
││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││
|
││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││
|
||||||
││ Monitored Titl│ ╭─────────────────────────────────────────────────╮ │ize ││
|
││ Monitored Titl│ ╭─────────────────────────────────────────────────╮ │ize ││
|
||||||
││=> 🏷 Test│ Quality Profile: │Lossless ▼ │ │.00 GB ││
|
││=> 🏷 Test│ Quality Profile: │Lossless ▼ │ │02.47 MB ││
|
||||||
││ │ ╰─────────────────────────────────────────────────╯ │ ││
|
││ │ ╰─────────────────────────────────────────────────╯ │ ││
|
||||||
││ │ ╭─────────────────────────────────────────────────╮ │ ││
|
││ │ ╭─────────────────────────────────────────────────╮ │ ││
|
||||||
││ │ Metadata Profile: │Standard ▼ │ │ ││
|
││ │ Metadata Profile: │Standard ▼ │ │ ││
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Artist: Alex │
|
│Artist: Alex │
|
||||||
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Artist: Alex │
|
│Artist: Alex │
|
||||||
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Artist: Alex │
|
│Artist: Alex │
|
||||||
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
||||||
@@ -24,7 +24,7 @@ expression: output
|
|||||||
││ Albums │ History │ Manual Search ││
|
││ Albums │ History │ Manual Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Monitored Title Type Tracks Duration Release Date Size ││
|
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||||
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 0.00 GB ││
|
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 302.47 MB ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: output
|
|||||||
---
|
---
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
|
||||||
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex
|
=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||