Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11524bb232
|
||
|
|
7a79e1c05e
|
||
|
|
06e504732d | ||
|
|
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
|
||
|
|
87edf1fb9e
|
||
|
|
d3a959d2dd
|
||
|
|
1857d5ba15
|
||
|
|
86b91ce6dd
|
||
|
|
9dbd985506
|
||
|
|
fbf711f428
|
||
|
|
db8e97a700
|
||
|
|
c069ed4a9b
|
||
|
|
3227495947
|
||
|
|
243b22a426
|
||
|
|
caafe1602e
|
||
|
|
65ba37c09a
|
||
|
|
c525fb9a4e
|
||
|
|
e79cc7a12d
|
||
|
|
12dbbf0b1b
|
||
|
|
1a5e5c3145
|
||
|
|
5fa521ba7f
|
||
|
|
f7c67ad58b
|
||
|
|
7e8072133c
|
||
|
|
e622be3288
|
||
|
|
08018e4457
|
||
|
|
78f5045844
|
||
|
|
e51b9d72b1
|
||
|
|
b9d96ce235
|
||
|
|
3e414f9f0c
|
||
|
|
f99d4f61f4
|
||
|
|
d08f7d4b4e
|
||
|
|
cef1ad13af
|
||
|
|
f141e53f30
|
||
|
|
18a50ab97c
|
||
|
|
4cbec79334
|
||
|
|
c9244b0459
|
||
|
|
b3ce15a564
|
||
|
|
ca04498717
|
||
|
|
27c458307f
|
||
|
|
ee6035c75d
|
||
|
|
3fd0a61737
|
||
|
|
720584ea17
|
||
|
|
59346973fc
|
||
|
|
a7178a481b
|
||
|
|
70441259a0
|
||
|
|
24c5dedd0c
|
||
|
|
58fe7d79ae
|
||
|
|
5e1f3bb7e3
|
||
|
|
a4a3c77586
|
||
|
|
ee07a0289f
|
||
|
|
91c2613e65
|
||
|
|
f73959b73f
|
||
|
|
bad43d1383
|
||
|
|
adf0896b7d
|
||
|
|
bd96c4ab23
|
||
|
|
e9d3851db8
|
||
|
|
773719dbd2
|
||
|
|
2b07ad96f1
|
||
|
|
7fa091cb44
|
||
|
|
7d1ff86c68
|
||
|
|
7500e2da24
|
||
|
|
6a7f9fe0da
|
||
|
|
dc4b11cf1b
|
||
|
|
ab9645dee9
|
||
|
|
da6c9e7b66
|
||
|
|
a311e2b895
|
||
|
|
61a18cb222
|
||
|
|
280e691a94
|
||
|
|
b4c6994362
|
||
|
|
d97c20d5ac
|
||
|
|
751ff6ecf5
|
||
|
|
14deed6b81
|
||
|
|
baf74d95e1
|
||
|
|
5480514b12
|
||
|
|
a8d13ae5c8
|
||
|
|
d1d5033710
|
||
|
|
fb465b06f8
|
||
|
|
06e2d7841e
|
||
|
|
d6deb6eea2
|
||
|
|
dbd84d929d
|
||
|
|
0867ae918e
|
||
|
|
cd5ef8e093
|
||
|
|
f4fd8516a6
|
||
|
|
aea7fd2607
|
||
|
|
daf991d1eb
|
||
|
|
be5826f2c4
|
||
|
|
c94b2a40d0
|
||
|
|
fb91c2edd6
|
||
|
|
5c593b3770
|
||
|
|
5a730f2f05
|
||
|
|
999a22a37f
|
||
|
|
666084a30f
|
||
|
|
1edceba056
|
||
|
|
40be5714e9
|
||
|
|
f5bcb7d83a
|
||
|
|
5c9fead725
|
||
|
|
6139b48d90
|
||
|
|
3569bcc036
|
||
|
|
fbfd64eb2a
|
||
|
|
e30d4dbd0c
|
||
|
|
f2787560d6
|
||
|
|
b48ce5db2e
|
||
|
|
634d1fc1b1
|
||
|
|
4c6a9ece4e
|
||
|
|
817ebeaf90
|
||
|
|
41830cc072
|
||
|
|
8752a85db5
|
||
|
|
9572b68f97
|
||
|
|
c82006862e
|
||
|
|
e9b9aa3a14
|
||
|
|
f896b65fd9
|
||
|
|
ac8a11233f
|
||
|
|
fcfc29894e
|
||
|
|
e514e9b6cc
|
||
|
|
9238c1b9d2
|
||
|
|
5042dbdb3a
|
||
|
|
817fe4d135
|
||
|
|
84eba9beac
|
||
|
|
e1be1df68a
|
||
|
|
3475660eb2
|
||
|
|
e8e3376327
|
||
|
|
b9fa3b36c0
|
||
|
|
29186ea03c
|
||
|
|
ce8c51b02f
|
||
|
|
0cc49089b6
|
||
|
|
1d8ccd7ff6
|
||
|
|
de41d6fef7
|
||
|
|
4a7cf922e8
|
||
|
|
44d147ce37
|
||
|
|
ab3992c372
|
||
|
|
bd0274411c
|
||
|
|
690a17e152
|
||
|
|
c6d729162a
|
||
|
|
e0e82e654e
|
||
|
|
97d67a6938
|
||
|
|
1072585903
|
||
|
|
520b422dcc
|
||
|
|
d8f171ec6f
|
||
|
|
cd341c3d71
|
||
|
|
29f043f5c7
|
||
|
|
4db9b92146
|
||
|
|
a362add8f0
|
||
|
|
e779f70187 | ||
|
|
a9054d9a98 | ||
|
|
645ca19d76 | ||
|
|
6f697f64f4 | ||
|
|
a72b2991fd | ||
|
|
6c41e0704e | ||
|
|
6e7707f6df | ||
|
|
a26444006b | ||
|
|
ccf3e28323 | ||
|
|
6eea6b92fb | ||
|
|
6df68b8a66
|
||
|
|
cbca6bd916
|
||
|
|
92187c5f16
|
||
|
|
366809d8c6
|
||
|
|
dd93fe117d
|
||
|
|
10e18af1bf
|
||
|
|
a4d93692a9
|
||
|
|
03d134bec9
|
||
|
|
4cad9e1755
|
||
|
|
adb76bb603 | ||
|
|
d9a2b1c6c4 | ||
|
|
47a2fbf0cc
|
||
|
|
7067902752
|
||
|
|
ba1cf0182b
|
||
|
|
5cccec88c9
|
||
|
|
bbcd3f00a9
|
||
|
|
2e339dd73b | ||
|
|
f988cf0f26
|
||
|
|
ff82dc2012
|
||
|
|
89a692ad90 | ||
|
|
d77ec5fb34 | ||
|
|
ec90e2dca7 | ||
|
|
5a4e6c9623
|
||
|
|
9a6a06ee20
|
||
|
|
5556e48fc0
|
||
|
|
af573cac2a
|
||
|
|
447cf6a2b4
|
||
|
|
203bf9cb66
|
||
|
|
4f9bc34d23
|
||
|
|
a2aa9507a9
|
||
|
|
c791b985f0
|
||
|
|
5c517a748c
|
||
|
|
892c687077
|
||
|
|
c6d5b98e86
|
||
|
|
67e5114ec2
|
||
|
|
fdc331865e
|
||
|
|
f388dccc08
|
||
|
|
64fad3b9bc
|
||
|
|
3be7b09da8
|
||
|
|
5f3123cd79
|
||
|
|
d8f7febfe1
|
||
|
|
0bfbb44e3e
|
||
|
|
c5161f828d | ||
|
|
71c64167f0 | ||
|
|
4d3e00fd94 | ||
|
|
9e96e74c87 | ||
|
|
ddb869c341 | ||
|
|
f17f542e8e | ||
|
|
a2e6400a38 | ||
|
|
89f5ff6bc7 | ||
|
|
eff1a901eb | ||
|
|
7add62b245 | ||
|
|
5fa9b08347 | ||
|
|
7bb5f83a56 | ||
|
|
caf4ad1e64 | ||
|
|
bc6ecc39f4 | ||
|
|
5e70d70758 | ||
|
|
1329589bd6 | ||
|
|
c6dc8f6090 | ||
|
|
0ee275d58f | ||
|
|
8dfa664a06 | ||
|
|
d7f0dd5950 | ||
|
|
8b9467bd39 | ||
|
|
c74d5936d2 | ||
|
|
8abcf44866 | ||
|
|
d2217509f2 | ||
|
|
c68cd75015 | ||
|
|
e1a25bfaf2 | ||
|
|
ad9e2b3671 | ||
|
|
0172253d20 | ||
|
|
47fdee190a | ||
|
|
68b08d1cd7 | ||
|
|
f31810e48a | ||
|
|
09bee7473f | ||
|
|
b2814371f0 | ||
|
|
269057867f | ||
|
|
450fdd7106 | ||
|
|
c624d1b9e4 | ||
|
|
e94f78dc7b | ||
|
|
b1a6db21f1 | ||
|
|
ca208ff5e4 | ||
|
|
1a43d1ec7c | ||
|
|
4abf705cb5 | ||
|
|
cf98b10d77 | ||
|
|
f0ed71b436 | ||
|
|
243de47cae | ||
|
|
d3947d9e15 | ||
|
|
64d8c65831 | ||
|
|
60c4cf1098 | ||
|
|
9cc3ccb419 | ||
|
|
45c61369c8 | ||
|
|
a8609e08c5 | ||
|
|
a18b047f4f | ||
|
|
b1afdaf541 | ||
|
|
3c1634d1e3 | ||
|
|
9b4eda6a9d | ||
|
|
96308afeee | ||
|
|
4e13d5d34d | ||
|
|
b4a99d1665 | ||
|
|
a012f6ecd5 | ||
|
|
5afee1998b | ||
|
|
059fa48bd9 | ||
|
|
6771a0ab38 | ||
|
|
bc3aeefa6e | ||
|
|
e61537942b | ||
|
|
5d09b2402c |
No files matched your search
@@ -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
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
toolchain: [stable, beta]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Run clippy action
|
||||
uses: clechasseur/rs-clippy-check@v3
|
||||
uses: clechasseur/rs-clippy-check@v5
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
name: nightly / doc
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
@@ -76,15 +76,42 @@ jobs:
|
||||
RUSTDOCFLAGS: --cfg docsrs
|
||||
msrv:
|
||||
# check that we can build using the minimal rust version that is specified by this crate
|
||||
name: 1.89.0 / check
|
||||
name: 1.95.0 / check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install 1.89.0
|
||||
- name: Install 1.95.0
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.89.0
|
||||
toolchain: 1.95.0
|
||||
|
||||
- name: cargo +1.89.0 check
|
||||
- name: cargo +1.95.0 check
|
||||
run: cargo check
|
||||
|
||||
dockerfile-msrv:
|
||||
# The Dockerfile's pinned Rust image is only built at release time, so a
|
||||
# rust-version bump that misses it fails silently until the next release.
|
||||
# Catch the drift on every push instead.
|
||||
name: dockerfile / msrv
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Check Dockerfile Rust image against Cargo.toml rust-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
msrv=$(sed -nE 's/^rust-version = "([0-9.]+)"$/\1/p' Cargo.toml)
|
||||
image_tag=$(sed -nE 's/^FROM rust:([0-9.]+) AS builder$/\1/p' Dockerfile)
|
||||
: "${msrv:?failed to extract rust-version from Cargo.toml}"
|
||||
: "${image_tag:?failed to extract the rust image tag from the Dockerfile}"
|
||||
# The rust image tag pins major.minor, so compare on major.minor only
|
||||
msrv_mm=$(echo "$msrv" | cut -d. -f1,2)
|
||||
image_mm=$(echo "$image_tag" | cut -d. -f1,2)
|
||||
if [ "$(printf '%s\n' "$msrv_mm" "$image_mm" | sort -V | head -n1)" != "$msrv_mm" ]; then
|
||||
echo "Dockerfile builds with rust:$image_tag, but Cargo.toml declares rust-version = \"$msrv\"."
|
||||
echo "Cargo refuses to build under an older toolchain, so the release Docker publish step will fail."
|
||||
echo "Update the FROM rust:* line in the Dockerfile to at least $msrv_mm."
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: Dockerfile rust:$image_tag satisfies rust-version $msrv"
|
||||
@@ -32,19 +32,19 @@ jobs:
|
||||
|
||||
- name: Checkout repository
|
||||
if: env.ACT != 'true'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout repository
|
||||
if: env.ACT == 'true'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
@@ -219,13 +219,13 @@ jobs:
|
||||
git push origin --follow-tags
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Upload the changed Cargo files (Act)
|
||||
if: env.ACT == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: bumped-cargo-files
|
||||
path: |
|
||||
@@ -267,7 +267,7 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -279,18 +279,18 @@ jobs:
|
||||
|
||||
- name: Get bumped Cargo files (Act)
|
||||
if: env.ACT == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: bumped-cargo-files
|
||||
path: ${{ github.workspace }}
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v5
|
||||
name: Cache Cargo registry
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v5
|
||||
if: startsWith(matrix.job.name, 'linux-')
|
||||
with:
|
||||
path: ~/.cargo/bin
|
||||
@@ -375,7 +375,7 @@ jobs:
|
||||
cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.sha256 artifacts/
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: artifacts-${{ env.RELEASE_NAME }}
|
||||
path: artifacts
|
||||
@@ -393,12 +393,12 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
@@ -421,7 +421,7 @@ jobs:
|
||||
|
||||
- name: Create a GitHub Release
|
||||
if: env.ACT != 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -457,7 +457,7 @@ jobs:
|
||||
prerelease: false
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: artifacts
|
||||
overwrite: true
|
||||
@@ -474,12 +474,12 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Get release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
@@ -528,12 +528,12 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Get release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
@@ -592,12 +592,12 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Get release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
@@ -618,20 +618,20 @@ jobs:
|
||||
echo "Release version: ${{ env.version }}"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: env.ACT != 'true'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
@@ -651,13 +651,13 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get bumped Cargo files (Act)
|
||||
if: env.ACT == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: bumped-cargo-files
|
||||
path: ${{ github.workspace }}
|
||||
@@ -668,13 +668,13 @@ jobs:
|
||||
git fetch --all
|
||||
git pull
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v5
|
||||
name: Cache Cargo registry
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cargo/bin
|
||||
key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }}
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
# toolchain
|
||||
toolchain: [stable, beta]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install ${{ matrix.toolchain }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
# runs-on: ubuntu-latest
|
||||
# name: ubuntu / stable / minimal-versions
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions/checkout@v5
|
||||
|
||||
# - name: Install Rust stable
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
# - run: vcpkg install openssl:x64-windows-static-md
|
||||
# if: runner.os == 'Windows'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
name: ubuntu / stable / coverage
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
|
||||
- name: Upload to codecov.io
|
||||
if: env.ACT != 'true'
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
/.idea/
|
||||
/.scannerwork/
|
||||
/.act/
|
||||
/.sisyphus/
|
||||
/.coyote/
|
||||
@@ -5,6 +5,127 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.7.3 (2026-06-25)
|
||||
|
||||
### Feat
|
||||
|
||||
- Implemented log rolling so the log file doesn't just grow exponentially [#60]
|
||||
|
||||
### Fix
|
||||
|
||||
- addressed code review comments
|
||||
- tail-logs subcommand follows log rollovers and sleeps to minimize idle CPU loops
|
||||
|
||||
### Refactor
|
||||
|
||||
- Refactored several usages of sort_by_key and match guards to utilize newer Rust version APIs
|
||||
|
||||
## v0.7.2 (2026-04-20)
|
||||
|
||||
### Feat
|
||||
|
||||
- Created a separate 'ssl' property for the config so users don't have to specify an ssl_cert_path to use SSL or use the uri workaround for HTTPS API access
|
||||
|
||||
## v0.7.1 (2026-02-04)
|
||||
|
||||
### Feat
|
||||
|
||||
- Added support for a system-wide notification popup mechanism that works across Servarrs
|
||||
- Implemented a 'config-path' command to print out the default Managarr configuration file path to help address #54
|
||||
- Full support for filtering disks and aggregating root folders in the UI's 'Stats' block
|
||||
- proper collapsing of root folder paths in the stats layer of the UI
|
||||
- Added config option to filter for specific disk space paths to display in the UI (CLI is unaffected)
|
||||
- Improved disk-space UI and CLI that shows the actual path being monitored instead of just a disk number
|
||||
- Implemented the forgotten lidarr list disk-space command
|
||||
|
||||
### Fix
|
||||
|
||||
- Improved the system notification feature so it can persist between modals
|
||||
- Sonarr API updated to somtimes allow either seeders or leechers to be null
|
||||
- Improved the first-time run behavior so that it outputs the default configuration file it tries to load to help users locate the file on first-runs
|
||||
- 'managarr config-path' should work without a pre-existing config already in place [#54]
|
||||
|
||||
### Refactor
|
||||
|
||||
- Removed the filtering of monitored_storage_paths from the networking module and migrated all of it to the UI
|
||||
|
||||
## v0.7.0 (2026-01-21)
|
||||
|
||||
### Feat
|
||||
|
||||
- Blocklist support in Lidarr in both the CLI and TUI
|
||||
- CLI and TUI support for track history and track details in Lidarr
|
||||
- Lidarr UI support for album details popup
|
||||
- Implemented TUI handler support for the Album Details popup in Lidarr
|
||||
- Bulk added CLI support for tracks and album functionalities in Lidarr
|
||||
- Implemented the manual artist discography search tab in Lidarr's artist details UI
|
||||
- Lidarr CLI support for downloading a release
|
||||
- CLI support for searching for discography releases in Lidarr
|
||||
- Added TUI and CLI support for viewing Artist history in Lidarr
|
||||
- Full Lidarr system support for both the CLI and TUI
|
||||
- Full CLI and TUI support for the Lidarr Indexers tab
|
||||
- Full support for adding a root folder in Lidarr from both the CLI and TUI
|
||||
- naive lidarr root folder tab implementation. Needs improved add logic
|
||||
- Downloads tab support in Lidarr
|
||||
- Created a History tab in the Radarr UI and created a list history command and mark-history-item-as-failed command for Radarr
|
||||
- Implemented the Lidarr History tab and CLI support
|
||||
- TUI support for deleting a Lidarr album from the artist details popup
|
||||
- CLI support for deleting an album from Lidarr
|
||||
- Completed support for viewing Lidarr artist details
|
||||
- Full CLI and TUI support for adding an artist to Lidarr
|
||||
- Include the Lidarr artist disambiguation in the title of the Edit Artist popup
|
||||
- Initial Lidarr support for searching for new artists
|
||||
- Lidarr CLI commands to list quality profiles and metadata profiles
|
||||
- Improved CLI readability by creating a separate Global Options section for global flags
|
||||
- CLI support for deleting a tag in Lidarr
|
||||
- Lidarr CLI support for listing and adding tags
|
||||
- Added CLI and TUI support for editing Lidarr artists
|
||||
- Support for updating all Lidarr artists in both the CLI and TUI
|
||||
- Added Lidarr CLI support for fetching the host config and the security config
|
||||
- Created Lidarr commands: 'get artist-details' and 'get system-status'
|
||||
- Fetch the artist members as part of the artist details query
|
||||
- Support for toggling the monitoring of a given artist via the CLI and TUI
|
||||
- Full support for deleting an artist via CLI and TUI
|
||||
- TUI support for Lidarr library
|
||||
- CLI support for listing artists
|
||||
- Improved UI speed and responsiveness
|
||||
|
||||
### Fix
|
||||
|
||||
- Sonarr network wasn't checking for the user to be using the sorting block when populating season details
|
||||
- Sonarr CLI was not properly filtering out episode and season releases when manually searching for releases
|
||||
- Sonarr manual search TUI and CLI incorrectly displaying the same unfiltered results for both season and episode searches
|
||||
- Slowed down the automatic text scrolling in tables so the text is readable
|
||||
- Expanded the history item details size so that it can include all the available information for a given item; was previously being cut off on some screens
|
||||
- Bug in submitting the update series prompt in the series details UI in Sonarr
|
||||
- Don't include Lidarr artist disambiguation in Edit popup title when it is empty
|
||||
- Refactored how quality profiles, language profiles, and metadata profiles are populated for each servarr so they sort using the ID to mimic the web UI better
|
||||
- Added the correct keybinding context to the Lidarr edit artist popup
|
||||
- Improved fault tolerance for search result tables and test all indexer results tables
|
||||
- Prevented additional empty slice errors in indexer tables
|
||||
- Fixed a bug in all Servarr implementations to not try to get the current selection of a search table when an error is returned from the API
|
||||
- Fixed an issue with the Managarr table that would incorrectly try to display things before is_loading was ready
|
||||
- Fixed a bug where the edit collection popup would not display when opening it from collection details
|
||||
|
||||
### Refactor
|
||||
|
||||
- Refactored the SonarrEvent enum to not unnecessarily wrap dual series_id and season_number values in a tuple when both values can be passed directly
|
||||
- Improved and simplified the implementation of history details for both Sonarr and Lidarr
|
||||
- Let serde serialize Add Series and Add Movie enums instead of calling to_string up front
|
||||
- Use is_multiple_of for the tick counter in the UI module
|
||||
- Updated all model tests to use purpose-built assertions to improve readability and maintainability
|
||||
- Updated all handler tests to use purpose built assertions to improve readability and maintainability
|
||||
- Used is_multiple_of to make life easier and cleaner in the app module
|
||||
- Refactored all cli tests to use purpose-built assertions
|
||||
- Improved test assertions in the app module
|
||||
- Created dedicated proptests and assertions to clean up the handler unit tests
|
||||
- Migrated the handle_table_events macro into a trait for better IDE support, created a TableEventAdapter wrapper for the KeyEventHandlers to make it so that the trait can be used properly and a simple function to replace the previous call to the handle_table_events macro
|
||||
- Simplified both the table_handler macro and the stateful_table implementation
|
||||
- Improved error handling for the tail-logs subcommand to propagate errors up the stack instead of exiting there.
|
||||
- Added accessor methods to servarr_data structs, replaced for loops with functional iterator chains, eliminated mutable state tracking, and updated network module to use get_or_insert_default() for modal options
|
||||
- Improved error handling project-wide and cleaned up some regexes with unnecessary escapes (tail_logs and interpolate_env_vars)
|
||||
- Refactored to use more idiomatic let-else statements where applicable
|
||||
|
||||
## v0.6.3 (2025-12-13)
|
||||
|
||||
### Fix
|
||||
|
||||
@@ -91,5 +91,12 @@ Then, you can run workflows locally without having to commit and see if the GitH
|
||||
act -W .github/workflows/release.yml --input_type bump=minor
|
||||
```
|
||||
|
||||
## Authorship Policy
|
||||
|
||||
All code in this repository is written and reviewed by humans, or AI-generated where explicitly disclosed. AI-generated code (e.g., Copilot, ChatGPT,
|
||||
Claude, etc.) is permitted only when explicitly disclosed and approved. For example: disclosing the use of AI-generated code in a PR description.
|
||||
|
||||
Submissions must certify that the contributor understands and can maintain the code they submit.
|
||||
|
||||
## Questions? Reach out to me!
|
||||
If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can!
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "managarr"
|
||||
version = "0.6.3"
|
||||
version = "0.7.3"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "A TUI and CLI to manage your Servarrs"
|
||||
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
|
||||
@@ -10,7 +10,7 @@ homepage = "https://github.com/Dark-Alex-17/managarr"
|
||||
readme = "README.md"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
rust-version = "1.89.0"
|
||||
rust-version = "1.95.0"
|
||||
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
|
||||
[workspace]
|
||||
@@ -20,67 +20,67 @@ members = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.68"
|
||||
backtrace = "0.3.74"
|
||||
anyhow = "1.0.100"
|
||||
backtrace = "0.3.76"
|
||||
bimap = { version = "0.6.3", features = ["serde"] }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
confy = { version = "0.6.0", default-features = false, features = [
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
confy = { version = "2.0.0", default-features = false, features = [
|
||||
"yaml_conf",
|
||||
] }
|
||||
crossterm = "0.28.1"
|
||||
derivative = "2.2.0"
|
||||
human-panic = "2.0.2"
|
||||
indoc = "2.0.0"
|
||||
log = "0.4.17"
|
||||
log4rs = { version = "1.2.0", features = ["file_appender"] }
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.9", features = ["json"] }
|
||||
serde_yaml = "0.9.16"
|
||||
serde_json = "1.0.91"
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
human-panic = "2.0.6"
|
||||
indoc = "2.0.7"
|
||||
log = "0.4.29"
|
||||
log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] }
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.13.2", features = ["json"] }
|
||||
serde_yaml = "0.9.34"
|
||||
serde_json = "1.0.149"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
strum_macros = "0.26.4"
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
tokio-util = "0.7.8"
|
||||
ratatui = { version = "0.29.0", features = [
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
tokio-util = "0.7.18"
|
||||
ratatui = { version = "0.30.0", features = [
|
||||
"all-widgets",
|
||||
"unstable-widget-ref",
|
||||
] }
|
||||
urlencoding = "2.1.2"
|
||||
clap = { version = "4.5.20", features = [
|
||||
urlencoding = "2.1.3"
|
||||
clap = { version = "4.5.56", features = [
|
||||
"derive",
|
||||
"cargo",
|
||||
"env",
|
||||
"wrap_help",
|
||||
] }
|
||||
clap_complete = "4.5.33"
|
||||
clap_complete = "4.5.65"
|
||||
itertools = "0.14.0"
|
||||
ctrlc = "3.4.5"
|
||||
colored = "3.0.0"
|
||||
async-trait = "0.1.83"
|
||||
ctrlc = "3.5.1"
|
||||
colored = "3.1.1"
|
||||
async-trait = "0.1.89"
|
||||
dirs-next = "2.0.0"
|
||||
managarr-tree-widget = "0.24.0"
|
||||
indicatif = "0.17.9"
|
||||
derive_setters = "0.1.6"
|
||||
deunicode = "1.6.0"
|
||||
openssl = { version = "0.10.70", features = ["vendored"] }
|
||||
managarr-tree-widget = "0.25.0"
|
||||
indicatif = "0.17.11"
|
||||
derive_setters = "0.1.9"
|
||||
deunicode = "1.6.2"
|
||||
openssl = { version = "0.10.79", features = ["vendored"] }
|
||||
veil = "0.2.0"
|
||||
validate_theme_derive = "0.1.0"
|
||||
enum_display_style_derive = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.16"
|
||||
mockall = "0.13.0"
|
||||
mockito = "1.0.0"
|
||||
pretty_assertions = "1.3.0"
|
||||
proptest = "1.6.0"
|
||||
assert_cmd = "2.1.2"
|
||||
mockall = "0.13.1"
|
||||
mockito = "1.7.1"
|
||||
pretty_assertions = "1.4.1"
|
||||
proptest = "1.9.0"
|
||||
rstest = "0.25.0"
|
||||
serial_test = "3.2.0"
|
||||
assertables = "9.8.2"
|
||||
insta = "1.41.1"
|
||||
serial_test = "3.3.1"
|
||||
assertables = "9.8.4"
|
||||
insta = "1.46.1"
|
||||
|
||||
[dev-dependencies.cargo-husky]
|
||||
version = "1"
|
||||
version = "1.5.0"
|
||||
default-features = false
|
||||
features = ["user-hooks"]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.89 AS builder
|
||||
FROM rust:1.95 AS builder
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Download and compile Rust dependencies in an empty project and cache as a separate Docker layer
|
||||
@@ -21,6 +21,8 @@ RUN mv target/release/managarr .
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the compiled binary from the builder container
|
||||
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||

|
||||

|
||||

|
||||

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

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

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

|
||||

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

|
||||

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

|
||||

|
||||

|
||||

|
||||

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

|
||||

|
||||

|
||||

|
||||

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

|
||||

|
||||
@@ -461,12 +662,12 @@ To see what feature(s) I'm currently working on, check out my [Wekan Board](http
|
||||
## Servarr Requirements
|
||||
* [Radarr >= 5.3.6.8612](https://radarr.video/docs/api/)
|
||||
* [Sonarr >= v4](https://sonarr.tv/docs/api/)
|
||||
* [Readarr v1](https://readarr.com/docs/api/)
|
||||
* [Lidarr v1](https://lidarr.audio/docs/api/)
|
||||
* [Readarr v1](https://readarr.com/docs/api/)
|
||||
* [Whisparr >= v3](https://whisparr.com/docs/api/)
|
||||
* [Prowlarr v1](https://prowlarr.com/docs/api/)
|
||||
* [Bazarr v1.1.4](http://localhost:6767/api)
|
||||
* [Tautulli >= v2](https://github.com/Tautulli/Tautulli/wiki/Tautulli-API-Reference)
|
||||
|
||||
## Creator
|
||||
* [Alex Clarke](https://github.com/Dark-Alex-17)
|
||||
* [Alex Clarke](https://github.com/Dark-Alex-17)
|
||||
@@ -1,7 +1,5 @@
|
||||
VERSION := "latest"
|
||||
IMG_NAME := "darkalex17/managarr"
|
||||
IMAGE := "{{IMG_NAME}}:{{VERSION}}"
|
||||
|
||||
|
||||
# List all recipes
|
||||
default:
|
||||
@@ -51,7 +49,7 @@ doctest:
|
||||
# Run all proptests
|
||||
[group: 'test']
|
||||
proptest:
|
||||
@cargo test proptest
|
||||
@cargo test -- property_tests proptest
|
||||
|
||||
alias test-snapshots := snapshot-tests
|
||||
# Run all snapshot tests
|
||||
@@ -87,5 +85,5 @@ build build_type='debug':
|
||||
|
||||
# Build the docker image
|
||||
[group: 'build']
|
||||
build-docker:
|
||||
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMAGE}}
|
||||
build-docker version=VERSION:
|
||||
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMG_NAME}}:{{version}} .
|
||||
@@ -5,3 +5,4 @@
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 56330c025ad79db641d0eb9f429ab74e95822e1fb015b58f0e158ea674cd42a1 # shrinks to list_size = 1, page_ops = 1
|
||||
cc 29df5ff471eb388b0ffde573a959a539e4cf32acd9e1cea5187bba2268dd57f7 # shrinks to list_size = 1, filter_term = "𑛀"
|
||||
@@ -1,5 +1,5 @@
|
||||
tab_spaces=2
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
reorder_imports = true
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
|
||||
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 319 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 |
@@ -5,14 +5,18 @@ mod tests {
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use serde_json::Value;
|
||||
use serial_test::serial;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||
use crate::network::NetworkEvent;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[test]
|
||||
@@ -31,10 +35,18 @@ mod tests {
|
||||
..ServarrConfig::default()
|
||||
};
|
||||
let sonarr_config_2 = ServarrConfig::default();
|
||||
let readarr_config_1 = ServarrConfig {
|
||||
name: Some("Readarr Test".to_owned()),
|
||||
weight: Some(2),
|
||||
..ServarrConfig::default()
|
||||
};
|
||||
let readarr_config_2 = ServarrConfig::default();
|
||||
let config = AppConfig {
|
||||
theme: None,
|
||||
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
||||
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
||||
lidarr: None,
|
||||
readarr: Some(vec![readarr_config_1.clone(), readarr_config_2.clone()]),
|
||||
};
|
||||
let expected_tab_routes = vec![
|
||||
TabRoute {
|
||||
@@ -43,6 +55,12 @@ mod tests {
|
||||
contextual_help: None,
|
||||
config: Some(sonarr_config_1),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Readarr Test".to_owned(),
|
||||
route: ActiveReadarrBlock::default().into(),
|
||||
contextual_help: None,
|
||||
config: Some(readarr_config_1),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Radarr 1".to_owned(),
|
||||
route: ActiveRadarrBlock::default().into(),
|
||||
@@ -61,6 +79,12 @@ mod tests {
|
||||
contextual_help: None,
|
||||
config: Some(sonarr_config_2),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Readarr 1".to_owned(),
|
||||
route: ActiveReadarrBlock::default().into(),
|
||||
contextual_help: None,
|
||||
config: Some(readarr_config_2),
|
||||
},
|
||||
];
|
||||
|
||||
let app = App::new(
|
||||
@@ -78,9 +102,9 @@ mod tests {
|
||||
assert_eq!(app.server_tabs.index, 0);
|
||||
assert_eq!(app.server_tabs.tabs, expected_tab_routes);
|
||||
assert_eq!(app.tick_until_poll, 400);
|
||||
assert_eq!(app.ticks_until_scroll, 4);
|
||||
assert_eq!(app.scroll_interval, Duration::from_millis(100));
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert!(!app.should_text_scroll);
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.is_routing);
|
||||
assert!(!app.should_refresh);
|
||||
@@ -99,7 +123,7 @@ mod tests {
|
||||
assert_eq!(app.error, HorizontallyScrollableText::default());
|
||||
assert_eq!(app.server_tabs.index, 0);
|
||||
assert_eq!(app.tick_until_poll, 400);
|
||||
assert_eq!(app.ticks_until_scroll, 4);
|
||||
assert_eq!(app.scroll_interval, Duration::from_millis(100));
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.is_routing);
|
||||
@@ -142,6 +166,36 @@ mod tests {
|
||||
assert!(app.is_routing);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_named_tab_uses_selected_servarr_route_and_network_events() {
|
||||
let (network_tx, mut network_rx) = mpsc::channel(500);
|
||||
let mut app = App::new(
|
||||
network_tx,
|
||||
AppConfig {
|
||||
radarr: Some(vec![ServarrConfig {
|
||||
name: Some("Movies".to_owned()),
|
||||
..ServarrConfig::default()
|
||||
}]),
|
||||
sonarr: Some(vec![ServarrConfig {
|
||||
name: Some("Shows".to_owned()),
|
||||
..ServarrConfig::default()
|
||||
}]),
|
||||
..AppConfig::default()
|
||||
},
|
||||
CancellationToken::new(),
|
||||
);
|
||||
|
||||
assert!(app.server_tabs.select_tab_by_title("Shows"));
|
||||
assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into());
|
||||
|
||||
app.on_tick().await;
|
||||
|
||||
assert_eq!(
|
||||
network_rx.recv().await.unwrap(),
|
||||
SonarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_cancellation_token() {
|
||||
let mut app = App {
|
||||
@@ -184,7 +238,9 @@ mod tests {
|
||||
..SonarrData::default()
|
||||
};
|
||||
let data = Data {
|
||||
lidarr_data: LidarrData::default(),
|
||||
radarr_data,
|
||||
readarr_data: ReadarrData::default(),
|
||||
sonarr_data,
|
||||
};
|
||||
let mut app = App {
|
||||
@@ -244,22 +300,20 @@ mod tests {
|
||||
#[test]
|
||||
fn test_on_ui_scroll_tick() {
|
||||
let mut app = App {
|
||||
ticks_until_scroll: 1,
|
||||
scroll_interval: Duration::ZERO,
|
||||
..App::default()
|
||||
};
|
||||
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
app.on_ui_scroll_tick();
|
||||
assert!(app.should_text_scroll);
|
||||
|
||||
app.on_ui_scroll_tick();
|
||||
assert!(app.should_text_scroll);
|
||||
|
||||
assert_eq!(app.ui_scroll_tick_count, 1);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
app.scroll_interval = Duration::from_secs(60);
|
||||
app.last_scroll = Instant::now();
|
||||
app.on_ui_scroll_tick();
|
||||
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert!(!app.should_text_scroll);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -444,6 +498,78 @@ mod tests {
|
||||
assert_none!(config.port);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_bool() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: true
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_string() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: "true"
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_present() {
|
||||
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL", "true") };
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL}
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_defaults_to_false() {
|
||||
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY", "test") };
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY}
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &false);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_optional_env_var_bool_empty() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_none!(config.ssl);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_header_map_is_present() {
|
||||
@@ -504,6 +630,56 @@ mod tests {
|
||||
assert_none!(config.custom_headers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_string_vec_is_present() {
|
||||
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION", "/path1") };
|
||||
let expected_monitored_paths = ["/path1", "/path2"];
|
||||
let yaml_data = r#"
|
||||
monitored_storage_paths:
|
||||
- ${TEST_VAR_DESERIALIZE_STRING_VEC_OPTION}
|
||||
- /path2
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_string_vec_does_not_overwrite_non_env_value() {
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
"TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE",
|
||||
"/path3",
|
||||
)
|
||||
};
|
||||
let expected_monitored_paths = ["/path1", "/path2"];
|
||||
let yaml_data = r#"
|
||||
monitored_storage_paths:
|
||||
- /path1
|
||||
- /path2
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_optional_env_var_string_vec_empty() {
|
||||
let yaml_data = r#"
|
||||
api_token: "test123"
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_none!(config.monitored_storage_paths);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_u16_env_var_is_present() {
|
||||
@@ -617,10 +793,11 @@ mod tests {
|
||||
let api_token = "thisisatest".to_owned();
|
||||
let api_token_file = "/root/.config/api_token".to_owned();
|
||||
let ssl_cert_path = "/some/path".to_owned();
|
||||
let monitored_storage = vec!["/path1".to_owned(), "/path2".to_owned()];
|
||||
let mut custom_headers = HeaderMap::new();
|
||||
custom_headers.insert("X-Custom-Header", "value".parse().unwrap());
|
||||
let expected_str = format!(
|
||||
"ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}) }}"
|
||||
"ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl: Some(true), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}"
|
||||
);
|
||||
let servarr_config = ServarrConfig {
|
||||
name: Some(name),
|
||||
@@ -631,7 +808,9 @@ mod tests {
|
||||
api_token: Some(api_token),
|
||||
api_token_file: Some(api_token_file),
|
||||
ssl_cert_path: Some(ssl_cert_path),
|
||||
ssl: Some(true),
|
||||
custom_headers: Some(custom_headers),
|
||||
monitored_storage_paths: Some(monitored_storage),
|
||||
};
|
||||
|
||||
assert_str_eq!(format!("{servarr_config:?}"), expected_str);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
|
||||
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
|
||||
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
|
||||
use crate::app::readarr::readarr_context_clues::ReadarrContextClueProvider;
|
||||
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
|
||||
use crate::models::Route;
|
||||
|
||||
@@ -21,6 +23,8 @@ impl ContextClueProvider for ServarrContextClueProvider {
|
||||
match app.get_current_route() {
|
||||
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
|
||||
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
|
||||
Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app),
|
||||
Route::Readarr(_, _) => ReadarrContextClueProvider::get_context_clues(app),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -100,6 +104,18 @@ pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
|
||||
),
|
||||
];
|
||||
|
||||
pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(DEFAULT_KEYBINDINGS.tasks, "open tasks"),
|
||||
(DEFAULT_KEYBINDINGS.events, "open events"),
|
||||
@@ -110,3 +126,8 @@ pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
];
|
||||
|
||||
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "start task"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
@@ -2,13 +2,14 @@
|
||||
mod test {
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
|
||||
ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||
ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||
ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
ServarrContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES, ServarrContextClueProvider,
|
||||
};
|
||||
use crate::app::{App, key_binding::DEFAULT_KEYBINDINGS};
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
|
||||
#[test]
|
||||
@@ -204,6 +205,40 @@ mod test {
|
||||
assert_none!(indexers_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_context_clues() {
|
||||
let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
|
||||
);
|
||||
assert_none!(history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_context_clues() {
|
||||
let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter();
|
||||
@@ -234,6 +269,21 @@ mod test {
|
||||
assert_none!(system_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_tasks_context_clues() {
|
||||
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
system_tasks_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "start task")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
system_tasks_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(system_tasks_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_servarr_context_clue_provider_delegates_to_radarr_provider() {
|
||||
let mut app = App::test_default();
|
||||
@@ -241,10 +291,7 @@ mod test {
|
||||
|
||||
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(
|
||||
context_clues,
|
||||
&crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
);
|
||||
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -254,10 +301,17 @@ mod test {
|
||||
|
||||
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(
|
||||
context_clues,
|
||||
&crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
);
|
||||
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_servarr_context_clue_provider_delegates_to_readarr_provider() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::SystemTasks.into());
|
||||
|
||||
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -27,6 +27,7 @@ generate_keybindings! {
|
||||
filter,
|
||||
sort,
|
||||
edit,
|
||||
view,
|
||||
logs,
|
||||
tasks,
|
||||
test,
|
||||
@@ -142,6 +143,11 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
|
||||
alt: None,
|
||||
desc: "edit",
|
||||
},
|
||||
view: KeyBinding {
|
||||
key: Key::Char('v'),
|
||||
alt: None,
|
||||
desc: "view",
|
||||
},
|
||||
events: KeyBinding {
|
||||
key: Key::Char('e'),
|
||||
alt: None,
|
||||
|
||||
@@ -26,6 +26,7 @@ mod test {
|
||||
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), None, "filter")]
|
||||
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")]
|
||||
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")]
|
||||
#[case(DEFAULT_KEYBINDINGS.view, Key::Char('v'), None, "view")]
|
||||
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), None, "events")]
|
||||
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), None, "logs")]
|
||||
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), None, "tasks")]
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS,
|
||||
ARTIST_OVERVIEW_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS,
|
||||
EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, TRACK_DETAILS_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_context_clues_tests.rs"]
|
||||
mod lidarr_context_clues_tests;
|
||||
|
||||
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 10] = [
|
||||
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
|
||||
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.update, "update all"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete album"),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
"toggle album monitoring",
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.view, "view overview"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static ARTIST_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static ARTIST_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
|
||||
];
|
||||
|
||||
pub static MANUAL_ARTIST_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit artist"),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static ALBUM_DETAILS_CONTEXT_CLUES: [ContextClue; 6] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "track details"),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete track"),
|
||||
];
|
||||
|
||||
pub static ALBUM_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
|
||||
];
|
||||
|
||||
pub static MANUAL_ALBUM_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static TRACK_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static TRACK_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
|
||||
];
|
||||
|
||||
pub(in crate::app) struct LidarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for LidarrContextClueProvider {
|
||||
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
|
||||
let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() else {
|
||||
panic!("LidarrContextClueProvider::get_context_clues called with non-Lidarr route");
|
||||
};
|
||||
|
||||
match active_lidarr_block {
|
||||
_ if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_info_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if ARTIST_OVERVIEW_BLOCKS.contains(&active_lidarr_block) => {
|
||||
Some(&ARTIST_OVERVIEW_CONTEXT_CLUES)
|
||||
}
|
||||
_ if ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.expect("album_details_modal is empty")
|
||||
.album_details_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.expect("album_details_modal is empty")
|
||||
.track_details_modal
|
||||
.as_ref()
|
||||
.expect("track_details_modal is empty")
|
||||
.track_details_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
ActiveLidarrBlock::AddArtistSearchInput
|
||||
| ActiveLidarrBlock::AddArtistEmptySearchResults
|
||||
| ActiveLidarrBlock::TestAllIndexers
|
||||
| ActiveLidarrBlock::SystemLogs
|
||||
| ActiveLidarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
|
||||
_ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block)
|
||||
|| EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block)
|
||||
|| INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block)
|
||||
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) =>
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
_ if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block)
|
||||
|| DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) =>
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistPrompt
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitor
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
|
||||
| ActiveLidarrBlock::AddArtistSelectQualityProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectMetadataProfile
|
||||
| ActiveLidarrBlock::AddArtistSelectRootFolder
|
||||
| ActiveLidarrBlock::AddArtistTagsInput
|
||||
| ActiveLidarrBlock::AddArtistAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
|
||||
_ if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
|
||||
Some(&ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveLidarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
|
||||
_ => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.main_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ALBUM_DETAILS_CONTEXT_CLUES,
|
||||
ALBUM_HISTORY_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES,
|
||||
ARTIST_OVERVIEW_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
|
||||
MANUAL_ALBUM_SEARCH_CONTEXT_CLUES, MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
|
||||
TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
INDEXER_SETTINGS_BLOCKS, LidarrData,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::{AlbumDetailsModal, TrackDetailsModal};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_artists_context_clues() {
|
||||
let mut artists_context_clues_iter = ARTISTS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, "update all")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
|
||||
);
|
||||
assert_none!(artists_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_context_clues() {
|
||||
let mut artist_details_context_clues_iter = ARTIST_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete album")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
"toggle album monitoring",
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.view, "view overview")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(artist_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_context_clues() {
|
||||
let mut artist_overview_context_clues_iter = ARTIST_OVERVIEW_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artist_overview_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_overview_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(artist_overview_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_search_results_context_clues() {
|
||||
let mut add_artist_search_results_context_clues_iter =
|
||||
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
add_artist_search_results_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
add_artist_search_results_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "edit search")
|
||||
);
|
||||
assert_none!(add_artist_search_results_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "LidarrContextClueProvider::get_context_clues called with non-Lidarr route"
|
||||
)]
|
||||
fn test_lidarr_context_clue_provider_get_context_clues_non_lidarr_route() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_history_context_clues() {
|
||||
let mut artist_history_context_clues_iter = ARTIST_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
|
||||
);
|
||||
assert_none!(artist_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_artist_search_context_clues() {
|
||||
let mut manual_artist_search_context_clues_iter = MANUAL_ARTIST_SEARCH_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
manual_artist_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_artist_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit artist")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_artist_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_artist_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_artist_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_artist_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_artist_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(manual_artist_search_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_details_context_clues() {
|
||||
let mut album_details_context_clues_iter = ALBUM_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
album_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "track details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete track")
|
||||
);
|
||||
assert_none!(album_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_history_context_clues() {
|
||||
let mut album_history_context_clues_iter = ALBUM_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
album_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
album_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
|
||||
);
|
||||
assert_none!(album_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_album_search_context_clues() {
|
||||
let mut manual_album_search_context_clues_iter = MANUAL_ALBUM_SEARCH_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
manual_album_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_album_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_album_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_album_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_album_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(manual_album_search_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_context_clues() {
|
||||
let mut track_details_context_clues_iter = TRACK_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
track_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(track_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_history_context_clues() {
|
||||
let mut track_history_context_clues_iter = TRACK_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
track_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
|
||||
);
|
||||
assert_none!(track_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveLidarrBlock::ArtistHistory, &ARTIST_HISTORY_CONTEXT_CLUES)]
|
||||
#[case(2, ActiveLidarrBlock::ManualArtistSearch, &MANUAL_ARTIST_SEARCH_CONTEXT_CLUES)]
|
||||
fn test_lidarr_context_clue_provider_artist_info_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data = LidarrData::default();
|
||||
app.data.lidarr_data.artist_info_tabs.set_index(index);
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::AlbumDetails, &ALBUM_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveLidarrBlock::AlbumHistory, &ALBUM_HISTORY_CONTEXT_CLUES)]
|
||||
#[case(2, ActiveLidarrBlock::ManualAlbumSearch, &MANUAL_ALBUM_SEARCH_CONTEXT_CLUES)]
|
||||
fn test_lidarr_context_clue_provider_album_details_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
let mut album_details_modal = AlbumDetailsModal::default();
|
||||
album_details_modal.album_details_tabs.set_index(index);
|
||||
let lidarr_data = LidarrData {
|
||||
album_details_modal: Some(album_details_modal),
|
||||
..LidarrData::default()
|
||||
};
|
||||
app.data.lidarr_data = lidarr_data;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::TrackDetails, &TRACK_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveLidarrBlock::TrackHistory, &TRACK_HISTORY_CONTEXT_CLUES)]
|
||||
fn test_lidarr_context_clue_provider_track_details_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_lidarr_block: ActiveLidarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
let mut track_details_modal = TrackDetailsModal::default();
|
||||
track_details_modal.track_details_tabs.set_index(index);
|
||||
let album_details_modal = AlbumDetailsModal {
|
||||
track_details_modal: Some(track_details_modal),
|
||||
..AlbumDetailsModal::default()
|
||||
};
|
||||
let lidarr_data = LidarrData {
|
||||
album_details_modal: Some(album_details_modal),
|
||||
..LidarrData::default()
|
||||
};
|
||||
app.data.lidarr_data = lidarr_data;
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artist_overview_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistOverview.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTIST_OVERVIEW_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artists_sort_prompt_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistsSortPrompt.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_search_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::SearchArtists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_filter_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::FilterArtists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_context_clue_provider_bare_popup_context_clues(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AddArtistSearchInput,
|
||||
ActiveLidarrBlock::AddArtistEmptySearchResults,
|
||||
ActiveLidarrBlock::TestAllIndexers
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() {
|
||||
let mut blocks = EDIT_ARTIST_BLOCKS.to_vec();
|
||||
blocks.extend(ADD_ROOT_FOLDER_BLOCKS);
|
||||
blocks.extend(INDEXER_SETTINGS_BLOCKS);
|
||||
blocks.extend(EDIT_INDEXER_BLOCKS);
|
||||
|
||||
for active_lidarr_block in blocks {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_add_artist_search_results_context_clues() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_add_artist_blocks(
|
||||
#[values(
|
||||
ActiveLidarrBlock::AddArtistPrompt,
|
||||
ActiveLidarrBlock::AddArtistSelectMonitor,
|
||||
ActiveLidarrBlock::AddArtistSelectMonitorNewItems,
|
||||
ActiveLidarrBlock::AddArtistSelectQualityProfile,
|
||||
ActiveLidarrBlock::AddArtistSelectMetadataProfile,
|
||||
ActiveLidarrBlock::AddArtistSelectRootFolder,
|
||||
ActiveLidarrBlock::AddArtistTagsInput,
|
||||
ActiveLidarrBlock::AddArtistAlreadyInLibrary
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_delete_artist_blocks(
|
||||
#[values(
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistConfirmPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion,
|
||||
ActiveLidarrBlock::DeleteArtistToggleDeleteFile
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_delete_album_blocks(
|
||||
#[values(
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
ActiveLidarrBlock::DeleteAlbumConfirmPrompt,
|
||||
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion,
|
||||
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_system_tasks_clues() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into());
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,783 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::models::lidarr_models::{Album, Artist, LidarrRelease};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
|
||||
use crate::models::servarr_models::Indexer;
|
||||
use crate::network::NetworkEvent;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
album, artist, track,
|
||||
};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_artists() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Artists)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_artist_details() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_blocklist_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Blocklist)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetBlocklist.into());
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_artist_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistHistory)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetArtistHistory(1).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_artist_search_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDiscographyReleases(1).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_artist_search_block_discography_releases_non_empty() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.discography_releases
|
||||
.set_items(vec![LidarrRelease::default()]);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_album_details_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}]);
|
||||
app.data.lidarr_data.albums.set_items(vec![Album {
|
||||
id: 1,
|
||||
..Album::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetTracks(1, 1).into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetTrackFiles(1).into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_album_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}]);
|
||||
app.data.lidarr_data.albums.set_items(vec![Album {
|
||||
id: 1,
|
||||
..Album::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumHistory)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetAlbumHistory(1, 1).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_album_history_block_no_op_when_albums_table_is_empty() {
|
||||
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumHistory)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_album_search_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}]);
|
||||
app.data.lidarr_data.albums.set_items(vec![Album {
|
||||
id: 1,
|
||||
..Album::default()
|
||||
}]);
|
||||
app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default());
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetAlbumReleases(1, 1).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_album_search_block_is_loading() {
|
||||
let mut app = App {
|
||||
is_loading: true,
|
||||
..App::test_default()
|
||||
};
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_album_search_block_album_releases_non_empty() {
|
||||
let mut app = App::test_default();
|
||||
let mut album_details_modal = AlbumDetailsModal::default();
|
||||
album_details_modal
|
||||
.album_releases
|
||||
.set_items(vec![LidarrRelease::default()]);
|
||||
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_downloads_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Downloads)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.add_artist_search = Some("test artist".into());
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AddArtistSearchResults)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::SearchNewArtist("test artist".to_owned()).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::History)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetHistory(500).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_root_folders_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::RootFolders)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_indexers_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Indexers)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetIndexers.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_all_indexer_settings_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::AllIndexerSettingsPrompt)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetAllIndexerSettings.into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_test_indexer_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.indexers.set_items(vec![Indexer {
|
||||
id: 1,
|
||||
..Indexer::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TestIndexer)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::TestIndexer(1).into());
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_test_all_indexers_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TestAllIndexers)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::TestAllIndexers.into()
|
||||
);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_system_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::System)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTasks.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetQueuedEvents.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetLogs(500).into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_system_updates_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::SystemUpdates)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetUpdates.into());
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_track_details_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetTrackDetails(5).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_track_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackHistory)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetTrackHistory(1, 1, 5).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_lidarr_prompt_action_no_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = false;
|
||||
|
||||
app.check_for_lidarr_prompt_action().await;
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_lidarr_prompt_action() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::GetStatus);
|
||||
|
||||
app.check_for_lidarr_prompt_action().await;
|
||||
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
|
||||
assert!(app.should_refresh);
|
||||
assert_eq!(app.data.lidarr_data.prompt_confirm_action, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_refresh_metadata() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
|
||||
app.refresh_lidarr_metadata().await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
|
||||
assert!(app.is_loading);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_on_tick_first_render() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_first_render = true;
|
||||
|
||||
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into());
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.is_first_render);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_on_tick_routing() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
|
||||
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
app.should_refresh = false;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
|
||||
|
||||
assert!(app.cancellation_token.is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_on_tick_should_refresh() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_on_tick_should_refresh_does_not_cancel_prompt_requests() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_loading = true;
|
||||
app.is_routing = true;
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(app.should_refresh);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert!(!app.cancellation_token.is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_on_tick_network_tick_frequency() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.tick_count = 2;
|
||||
app.tick_until_poll = 2;
|
||||
|
||||
app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_add_new_artist_search_query() {
|
||||
let app = App::test_default_fully_populated();
|
||||
|
||||
let query = app.extract_add_new_artist_search_query().await;
|
||||
|
||||
assert_str_eq!(query, "Test Artist");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic(expected = "Add artist search is empty")]
|
||||
async fn test_extract_add_new_artist_search_query_panics_when_the_query_is_not_set() {
|
||||
let app = App::test_default();
|
||||
|
||||
app.extract_add_new_artist_search_query().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_artist_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(vec![artist()]);
|
||||
|
||||
assert_eq!(app.extract_artist_id().await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_album_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.albums.set_items(vec![album()]);
|
||||
|
||||
assert_eq!(app.extract_album_id().await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_track_id() {
|
||||
let mut app = App::test_default();
|
||||
let mut album_details_modal = AlbumDetailsModal::default();
|
||||
album_details_modal.tracks.set_items(vec![track()]);
|
||||
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
|
||||
|
||||
assert_eq!(app.extract_track_id().await, 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic(expected = "album_details_modal is empty")]
|
||||
async fn test_extract_track_id_panics_when_album_details_modal_is_not_set() {
|
||||
let app = App::test_default();
|
||||
|
||||
app.extract_track_id().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_lidarr_indexer_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.indexers.set_items(vec![Indexer {
|
||||
id: 1,
|
||||
..Indexer::default()
|
||||
}]);
|
||||
|
||||
assert_eq!(app.extract_lidarr_indexer_id().await, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
use super::App;
|
||||
use crate::{
|
||||
models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
|
||||
pub mod lidarr_context_clues;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_tests.rs"]
|
||||
mod lidarr_tests;
|
||||
|
||||
impl App<'_> {
|
||||
pub(super) async fn dispatch_by_lidarr_block(&mut self, active_lidarr_block: &ActiveLidarrBlock) {
|
||||
match active_lidarr_block {
|
||||
ActiveLidarrBlock::Artists => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::ListArtists.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::Blocklist => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetBlocklist.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::Downloads => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::ArtistDetails => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::ArtistHistory => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::GetArtistHistory(self.extract_artist_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::ManualArtistSearch => {
|
||||
if self.data.lidarr_data.discography_releases.is_empty() {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::GetDiscographyReleases(self.extract_artist_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::AlbumDetails => {
|
||||
let artist_id = self.extract_artist_id().await;
|
||||
let album_id = self.extract_album_id().await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTracks(artist_id, album_id).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTrackFiles(album_id).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::AlbumHistory => {
|
||||
if !self.data.lidarr_data.albums.is_empty() {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::GetAlbumHistory(
|
||||
self.extract_artist_id().await,
|
||||
self.extract_album_id().await,
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::ManualAlbumSearch => {
|
||||
match self.data.lidarr_data.album_details_modal.as_ref() {
|
||||
Some(album_details_modal) if album_details_modal.album_releases.is_empty() => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::GetAlbumReleases(
|
||||
self.extract_artist_id().await,
|
||||
self.extract_album_id().await,
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistSearchResults => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::SearchNewArtist(self.extract_add_new_artist_search_query().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::History => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetHistory(500).into())
|
||||
.await
|
||||
}
|
||||
ActiveLidarrBlock::RootFolders => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::Indexers => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetIndexers.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::AllIndexerSettingsPrompt => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetAllIndexerSettings.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::TestIndexer => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::TestIndexer(self.extract_lidarr_indexer_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::TestAllIndexers => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::TestAllIndexers.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::System => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTasks.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetQueuedEvents.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetLogs(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::SystemUpdates => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetUpdates.into())
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::TrackDetails => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::GetTrackDetails(self.extract_track_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveLidarrBlock::TrackHistory => {
|
||||
let artist_id = self.extract_artist_id().await;
|
||||
let album_id = self.extract_album_id().await;
|
||||
let track_id = self.extract_track_id().await;
|
||||
self
|
||||
.dispatch_network_event(
|
||||
LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.check_for_lidarr_prompt_action().await;
|
||||
self.reset_tick_count();
|
||||
}
|
||||
|
||||
async fn extract_add_new_artist_search_query(&self) -> String {
|
||||
self
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_search
|
||||
.as_ref()
|
||||
.expect("Add artist search is empty")
|
||||
.text
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn extract_artist_id(&self) -> i64 {
|
||||
self.data.lidarr_data.artists.current_selection().id
|
||||
}
|
||||
|
||||
async fn extract_album_id(&self) -> i64 {
|
||||
self.data.lidarr_data.albums.current_selection().id
|
||||
}
|
||||
|
||||
async fn extract_track_id(&self) -> i64 {
|
||||
self
|
||||
.data
|
||||
.lidarr_data
|
||||
.album_details_modal
|
||||
.as_ref()
|
||||
.expect("album_details_modal is empty")
|
||||
.tracks
|
||||
.current_selection()
|
||||
.id
|
||||
}
|
||||
|
||||
async fn extract_lidarr_indexer_id(&self) -> i64 {
|
||||
self.data.lidarr_data.indexers.current_selection().id
|
||||
}
|
||||
|
||||
async fn check_for_lidarr_prompt_action(&mut self) {
|
||||
if self.data.lidarr_data.prompt_confirm {
|
||||
self.data.lidarr_data.prompt_confirm = false;
|
||||
if let Some(lidarr_event) = self.data.lidarr_data.prompt_confirm_action.take() {
|
||||
self.dispatch_network_event(lidarr_event.into()).await;
|
||||
self.should_refresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn lidarr_on_tick(&mut self, active_lidarr_block: ActiveLidarrBlock) {
|
||||
if self.is_first_render {
|
||||
self.refresh_lidarr_metadata().await;
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
self.is_first_render = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if self.should_refresh {
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
self.refresh_lidarr_metadata().await;
|
||||
}
|
||||
|
||||
if self.is_routing {
|
||||
if !self.should_refresh {
|
||||
self.cancellation_token.cancel();
|
||||
} else {
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
}
|
||||
}
|
||||
|
||||
if self.tick_count.is_multiple_of(self.tick_until_poll) {
|
||||
self.refresh_lidarr_metadata().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_lidarr_metadata(&mut self) {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDiskSpace.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetStatus.into())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,17 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fs, process};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use veil::Redact;
|
||||
|
||||
use crate::cli::Command;
|
||||
use crate::models::servarr_data::Notification;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
@@ -25,7 +29,9 @@ mod app_tests;
|
||||
pub mod context_clues;
|
||||
pub mod key_binding;
|
||||
mod key_binding_tests;
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod readarr;
|
||||
pub mod sonarr;
|
||||
|
||||
pub struct App<'a> {
|
||||
@@ -36,10 +42,12 @@ pub struct App<'a> {
|
||||
pub server_tabs: TabState,
|
||||
pub keymapping_table: Option<StatefulTable<KeybindingItem>>,
|
||||
pub error: HorizontallyScrollableText,
|
||||
pub notification: Option<Notification>,
|
||||
pub tick_until_poll: u64,
|
||||
pub ticks_until_scroll: u64,
|
||||
pub scroll_interval: Duration,
|
||||
pub last_scroll: Instant,
|
||||
pub tick_count: u64,
|
||||
pub ui_scroll_tick_count: u64,
|
||||
pub should_text_scroll: bool,
|
||||
pub is_routing: bool,
|
||||
pub is_loading: bool,
|
||||
pub should_refresh: bool,
|
||||
@@ -96,6 +104,46 @@ impl App<'_> {
|
||||
server_tabs.extend(sonarr_tabs);
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = config.lidarr {
|
||||
let mut unnamed_idx = 0;
|
||||
let lidarr_tabs = lidarr_configs.into_iter().map(|lidarr_config| {
|
||||
let name = if let Some(name) = lidarr_config.name.clone() {
|
||||
name
|
||||
} else {
|
||||
unnamed_idx += 1;
|
||||
format!("Lidarr {unnamed_idx}")
|
||||
};
|
||||
|
||||
TabRoute {
|
||||
title: name,
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(lidarr_config),
|
||||
}
|
||||
});
|
||||
server_tabs.extend(lidarr_tabs);
|
||||
}
|
||||
|
||||
if let Some(readarr_configs) = config.readarr {
|
||||
let mut unnamed_idx = 0;
|
||||
let readarr_tabs = readarr_configs.into_iter().map(|readarr_config| {
|
||||
let name = if let Some(name) = readarr_config.name.clone() {
|
||||
name
|
||||
} else {
|
||||
unnamed_idx += 1;
|
||||
format!("Readarr {unnamed_idx}")
|
||||
};
|
||||
|
||||
TabRoute {
|
||||
title: name,
|
||||
route: ActiveReadarrBlock::Authors.into(),
|
||||
contextual_help: None,
|
||||
config: Some(readarr_config),
|
||||
}
|
||||
});
|
||||
server_tabs.extend(readarr_tabs);
|
||||
}
|
||||
|
||||
let weight_sorted_tabs = server_tabs
|
||||
.into_iter()
|
||||
.sorted_by(|tab1, tab2| {
|
||||
@@ -147,10 +195,11 @@ impl App<'_> {
|
||||
}
|
||||
|
||||
pub fn on_ui_scroll_tick(&mut self) {
|
||||
if self.ui_scroll_tick_count == self.ticks_until_scroll {
|
||||
self.ui_scroll_tick_count = 0;
|
||||
if self.last_scroll.elapsed() >= self.scroll_interval {
|
||||
self.should_text_scroll = true;
|
||||
self.last_scroll = Instant::now();
|
||||
} else {
|
||||
self.ui_scroll_tick_count += 1;
|
||||
self.should_text_scroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +225,8 @@ impl App<'_> {
|
||||
match self.get_current_route() {
|
||||
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
|
||||
Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
|
||||
Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await,
|
||||
Route::Readarr(active_readarr_block, _) => self.readarr_on_tick(active_readarr_block).await,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -212,14 +263,11 @@ impl App<'_> {
|
||||
}
|
||||
|
||||
pub fn get_current_route(&self) -> Route {
|
||||
*self.navigation_stack.last().unwrap_or(
|
||||
&self
|
||||
.server_tabs
|
||||
.tabs
|
||||
.first()
|
||||
.expect("At least one server tab must exist")
|
||||
.route,
|
||||
)
|
||||
self
|
||||
.navigation_stack
|
||||
.last()
|
||||
.copied()
|
||||
.unwrap_or_else(|| self.server_tabs.get_active_route())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,12 +279,14 @@ impl Default for App<'_> {
|
||||
cancellation_token: CancellationToken::new(),
|
||||
keymapping_table: None,
|
||||
error: HorizontallyScrollableText::default(),
|
||||
notification: None,
|
||||
is_first_render: true,
|
||||
server_tabs: TabState::new(Vec::new()),
|
||||
tick_until_poll: 400,
|
||||
ticks_until_scroll: 4,
|
||||
scroll_interval: Duration::from_millis(100),
|
||||
last_scroll: Instant::now(),
|
||||
tick_count: 0,
|
||||
ui_scroll_tick_count: 0,
|
||||
should_text_scroll: false,
|
||||
is_loading: false,
|
||||
is_routing: false,
|
||||
should_refresh: false,
|
||||
@@ -264,6 +314,18 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Lidarr".to_owned(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Readarr".to_owned(),
|
||||
route: ActiveReadarrBlock::Authors.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
@@ -272,7 +334,9 @@ impl App<'_> {
|
||||
pub fn test_default_fully_populated() -> Self {
|
||||
App {
|
||||
data: Data {
|
||||
lidarr_data: LidarrData::test_default_fully_populated(),
|
||||
radarr_data: RadarrData::test_default_fully_populated(),
|
||||
readarr_data: ReadarrData::test_default_fully_populated(),
|
||||
sonarr_data: SonarrData::test_default_fully_populated(),
|
||||
},
|
||||
server_tabs: TabState::new(vec![
|
||||
@@ -288,31 +352,55 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Lidarr".to_owned(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Readarr".to_owned(),
|
||||
route: ActiveReadarrBlock::Authors.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_navigation_stack_depth(&self) -> usize {
|
||||
self.navigation_stack.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Data<'a> {
|
||||
pub lidarr_data: LidarrData<'a>,
|
||||
pub radarr_data: RadarrData<'a>,
|
||||
pub readarr_data: ReadarrData<'a>,
|
||||
pub sonarr_data: SonarrData<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub theme: Option<String>,
|
||||
pub lidarr: Option<Vec<ServarrConfig>>,
|
||||
pub radarr: Option<Vec<ServarrConfig>>,
|
||||
pub readarr: Option<Vec<ServarrConfig>>,
|
||||
pub sonarr: Option<Vec<ServarrConfig>>,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn validate(&self) {
|
||||
if self.radarr.is_none() && self.sonarr.is_none() {
|
||||
log_and_print_error(
|
||||
"No Servarr configuration provided in the specified configuration file".to_owned(),
|
||||
);
|
||||
pub fn validate(&self, config_path: &str) {
|
||||
if self.lidarr.is_none()
|
||||
&& self.radarr.is_none()
|
||||
&& self.sonarr.is_none()
|
||||
&& self.readarr.is_none()
|
||||
{
|
||||
log_and_print_error(format!(
|
||||
"No Servarrs are configured in the file: {config_path}"
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
@@ -323,6 +411,14 @@ impl AppConfig {
|
||||
if let Some(sonarr_configs) = &self.sonarr {
|
||||
sonarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = &self.lidarr {
|
||||
lidarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
|
||||
if let Some(readarr_configs) = &self.readarr {
|
||||
readarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_config_present_for_cli(&self, command: &Command) {
|
||||
@@ -340,6 +436,14 @@ impl AppConfig {
|
||||
msg("Sonarr");
|
||||
process::exit(1);
|
||||
}
|
||||
Command::Lidarr(_) if self.lidarr.is_none() => {
|
||||
msg("Lidarr");
|
||||
process::exit(1);
|
||||
}
|
||||
Command::Readarr(_) if self.readarr.is_none() => {
|
||||
msg("Readarr");
|
||||
process::exit(1);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -356,6 +460,18 @@ impl AppConfig {
|
||||
sonarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(lidarr_configs) = self.lidarr.as_mut() {
|
||||
for lidarr_config in lidarr_configs {
|
||||
lidarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(readarr_configs) = self.readarr.as_mut() {
|
||||
for readarr_config in readarr_configs {
|
||||
readarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +492,8 @@ pub struct ServarrConfig {
|
||||
pub api_token: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
|
||||
pub api_token_file: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var_bool")]
|
||||
pub ssl: Option<bool>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
|
||||
pub ssl_cert_path: Option<String>,
|
||||
#[serde(
|
||||
@@ -384,6 +502,8 @@ pub struct ServarrConfig {
|
||||
serialize_with = "serialize_header_map"
|
||||
)]
|
||||
pub custom_headers: Option<HeaderMap>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var_string_vec")]
|
||||
pub monitored_storage_paths: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ServarrConfig {
|
||||
@@ -429,7 +549,9 @@ impl Default for ServarrConfig {
|
||||
api_token: Some(String::new()),
|
||||
api_token_file: None,
|
||||
ssl_cert_path: None,
|
||||
ssl: None,
|
||||
custom_headers: None,
|
||||
monitored_storage_paths: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -474,6 +596,29 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_env_var_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum StringOrBool {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
}
|
||||
|
||||
match StringOrBool::deserialize(deserializer)? {
|
||||
StringOrBool::Bool(b) => Ok(Some(b)),
|
||||
StringOrBool::String(s) => {
|
||||
let val = interpolate_env_vars(&s)
|
||||
.to_lowercase()
|
||||
.parse()
|
||||
.unwrap_or(false);
|
||||
Ok(Some(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_env_var_header_map<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<HeaderMap>, D::Error>
|
||||
@@ -496,6 +641,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_env_var_string_vec<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<String>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt: Option<Vec<String>> = Option::deserialize(deserializer)?;
|
||||
match opt {
|
||||
Some(vec) => Ok(Some(
|
||||
vec
|
||||
.into_iter()
|
||||
.map(|it| interpolate_env_vars(&it))
|
||||
.collect(),
|
||||
)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
|
||||
@@ -62,6 +62,11 @@ impl App<'_> {
|
||||
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveRadarrBlock::History => {
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetHistory(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveRadarrBlock::Indexers => {
|
||||
self
|
||||
.dispatch_network_event(RadarrEvent::GetTags.into())
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ADD_MOVIE_BLOCKS, ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
EDIT_MOVIE_BLOCKS, INDEXER_SETTINGS_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||
ADD_MOVIE_BLOCKS, ActiveRadarrBlock, DELETE_MOVIE_BLOCKS, EDIT_COLLECTION_BLOCKS,
|
||||
EDIT_INDEXER_BLOCKS, EDIT_MOVIE_BLOCKS, INDEXER_SETTINGS_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -82,11 +83,6 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "start task"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "show overview/add movie"),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit collection"),
|
||||
@@ -124,6 +120,9 @@ impl ContextClueProvider for RadarrContextClueProvider {
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
_ if DELETE_MOVIE_BLOCKS.contains(&active_radarr_block) => {
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveRadarrBlock::AddMoviePrompt
|
||||
| ActiveRadarrBlock::AddMovieSelectMonitor
|
||||
| ActiveRadarrBlock::AddMovieSelectMinimumAvailability
|
||||
|
||||
@@ -3,14 +3,15 @@ mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
|
||||
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
|
||||
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::radarr::radarr_context_clues::{
|
||||
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES,
|
||||
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
|
||||
MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider, SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
@@ -414,6 +415,25 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_delete_movie_blocks(
|
||||
#[values(
|
||||
ActiveRadarrBlock::DeleteMoviePrompt,
|
||||
ActiveRadarrBlock::DeleteMovieConfirmPrompt,
|
||||
ActiveRadarrBlock::DeleteMovieToggleAddListExclusion,
|
||||
ActiveRadarrBlock::DeleteMovieToggleDeleteFile
|
||||
)]
|
||||
active_radarr_block: ActiveRadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data = RadarrData::default();
|
||||
app.push_navigation_stack(active_radarr_block.into());
|
||||
|
||||
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_radarr_context_clue_provider_add_movie_search_results_context_clues(
|
||||
#[values(
|
||||
@@ -459,9 +479,10 @@ mod tests {
|
||||
#[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)]
|
||||
#[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)]
|
||||
#[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)]
|
||||
#[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
|
||||
#[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
|
||||
#[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
|
||||
#[case(4, ActiveRadarrBlock::History, &HISTORY_CONTEXT_CLUES)]
|
||||
#[case(5, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
|
||||
#[case(6, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
|
||||
#[case(7, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
|
||||
fn test_radarr_context_clue_provider_radarr_blocks_context_clues(
|
||||
#[case] index: usize,
|
||||
#[case] active_radarr_block: ActiveRadarrBlock,
|
||||
|
||||
@@ -6,7 +6,8 @@ mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::radarr::ActiveRadarrBlock;
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease,
|
||||
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, MinimumAvailability, Movie,
|
||||
MovieMonitor, RadarrRelease,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||
use crate::models::servarr_models::Indexer;
|
||||
@@ -88,13 +89,13 @@ mod tests {
|
||||
tmdb_id: 1234,
|
||||
title: "Test".to_owned(),
|
||||
root_folder_path: "/nfs2".to_owned(),
|
||||
minimum_availability: "announced".to_owned(),
|
||||
minimum_availability: MinimumAvailability::Announced,
|
||||
monitored: true,
|
||||
quality_profile_id: 2222,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
add_options: AddMovieOptions {
|
||||
monitor: "movieOnly".to_owned(),
|
||||
monitor: MovieMonitor::MovieOnly,
|
||||
search_for_movie: true,
|
||||
},
|
||||
};
|
||||
@@ -146,6 +147,23 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_history_block() {
|
||||
let (mut app, mut sync_network_rx) = construct_app_unit();
|
||||
|
||||
app
|
||||
.dispatch_by_radarr_block(&ActiveRadarrBlock::History)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
RadarrEvent::GetHistory(500).into()
|
||||
);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_root_folders_block() {
|
||||
let (mut app, mut sync_network_rx) = construct_app_unit();
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
use super::App;
|
||||
use crate::{
|
||||
models::servarr_data::readarr::readarr_data::ActiveReadarrBlock,
|
||||
network::readarr_network::ReadarrEvent,
|
||||
};
|
||||
|
||||
pub mod readarr_context_clues;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "readarr_tests.rs"]
|
||||
mod readarr_tests;
|
||||
|
||||
impl App<'_> {
|
||||
pub(super) async fn dispatch_by_readarr_block(
|
||||
&mut self,
|
||||
active_readarr_block: &ActiveReadarrBlock,
|
||||
) {
|
||||
match active_readarr_block {
|
||||
ActiveReadarrBlock::Authors => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::ListAuthors.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::Blocklist => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetBlocklist.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::Downloads => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::AuthorDetails => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetBooks(self.extract_author_id().await).into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::AuthorHistory => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::GetAuthorHistory(self.extract_author_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::ManualAuthorSearch => {
|
||||
if self.data.readarr_data.author_releases.is_empty() {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::GetAuthorReleases(self.extract_author_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::BookDetails => {
|
||||
let book_id = self.extract_book_id().await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetBookEditions(book_id).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetBookFiles(book_id).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::BookHistory => {
|
||||
if !self.data.readarr_data.books.is_empty() {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::GetBookHistory(
|
||||
self.extract_author_id().await,
|
||||
self.extract_book_id().await,
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::ManualBookSearch => {
|
||||
match self.data.readarr_data.book_details_modal.as_ref() {
|
||||
Some(book_details_modal) if book_details_modal.book_releases.is_empty() => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::GetBookReleases(self.extract_book_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::AddAuthorSearchResults => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::SearchNewAuthor(self.extract_add_new_author_search_query().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::History => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetHistory(500).into())
|
||||
.await
|
||||
}
|
||||
ActiveReadarrBlock::RootFolders => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::Indexers => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetIndexers.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetAllIndexerSettings.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::TestIndexer => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::TestIndexer(self.extract_readarr_indexer_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::TestAllIndexers => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::TestAllIndexers.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::System => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetTasks.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetQueuedEvents.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetLogs(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::SystemUpdates => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetUpdates.into())
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.check_for_readarr_prompt_action().await;
|
||||
self.reset_tick_count();
|
||||
}
|
||||
|
||||
async fn extract_add_new_author_search_query(&self) -> String {
|
||||
self
|
||||
.data
|
||||
.readarr_data
|
||||
.add_author_search
|
||||
.as_ref()
|
||||
.expect("Add author search is empty")
|
||||
.text
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn extract_author_id(&self) -> i64 {
|
||||
self.data.readarr_data.authors.current_selection().id
|
||||
}
|
||||
|
||||
async fn extract_book_id(&self) -> i64 {
|
||||
self.data.readarr_data.books.current_selection().id
|
||||
}
|
||||
|
||||
async fn extract_readarr_indexer_id(&self) -> i64 {
|
||||
self.data.readarr_data.indexers.current_selection().id
|
||||
}
|
||||
|
||||
async fn check_for_readarr_prompt_action(&mut self) {
|
||||
if self.data.readarr_data.prompt_confirm {
|
||||
self.data.readarr_data.prompt_confirm = false;
|
||||
if let Some(readarr_event) = self.data.readarr_data.prompt_confirm_action.take() {
|
||||
self.dispatch_network_event(readarr_event.into()).await;
|
||||
self.should_refresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn readarr_on_tick(&mut self, active_readarr_block: ActiveReadarrBlock) {
|
||||
if self.is_first_render {
|
||||
self.refresh_readarr_metadata().await;
|
||||
self.dispatch_by_readarr_block(&active_readarr_block).await;
|
||||
self.is_first_render = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if self.should_refresh {
|
||||
self.dispatch_by_readarr_block(&active_readarr_block).await;
|
||||
self.refresh_readarr_metadata().await;
|
||||
}
|
||||
|
||||
if self.is_routing {
|
||||
if !self.should_refresh {
|
||||
self.cancellation_token.cancel();
|
||||
} else {
|
||||
self.dispatch_by_readarr_block(&active_readarr_block).await;
|
||||
}
|
||||
}
|
||||
|
||||
if self.tick_count.is_multiple_of(self.tick_until_poll) {
|
||||
self.refresh_readarr_metadata().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_readarr_metadata(&mut self) {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetDiskSpace.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetStatus.into())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ADD_AUTHOR_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS,
|
||||
ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS,
|
||||
EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS, EDITION_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "readarr_context_clues_tests.rs"]
|
||||
mod readarr_context_clues_tests;
|
||||
|
||||
pub static AUTHORS_CONTEXT_CLUES: [ContextClue; 10] = [
|
||||
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
|
||||
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.update, "update all"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
pub static ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static AUTHOR_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit author"),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete book"),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
"toggle book monitoring",
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.view, "view overview"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static AUTHOR_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static AUTHOR_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit author"),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
|
||||
];
|
||||
|
||||
pub static MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit author"),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static BOOK_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "edition details"),
|
||||
];
|
||||
|
||||
pub static BOOK_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
|
||||
];
|
||||
|
||||
pub static BOOK_FILE_CONTEXT_CLUES: [ContextClue; 3] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete book file"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static MANUAL_BOOK_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static EDITION_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub(in crate::app) struct ReadarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for ReadarrContextClueProvider {
|
||||
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
|
||||
let Route::Readarr(active_readarr_block, _context_option) = app.get_current_route() else {
|
||||
panic!("ReadarrContextClueProvider::get_context_clues called with non-Readarr route");
|
||||
};
|
||||
|
||||
match active_readarr_block {
|
||||
_ if AUTHOR_DETAILS_BLOCKS.contains(&active_readarr_block) => app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_info_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if AUTHOR_OVERVIEW_BLOCKS.contains(&active_readarr_block) => {
|
||||
Some(&AUTHOR_OVERVIEW_CONTEXT_CLUES)
|
||||
}
|
||||
_ if BOOK_DETAILS_BLOCKS.contains(&active_readarr_block) => app
|
||||
.data
|
||||
.readarr_data
|
||||
.book_details_modal
|
||||
.as_ref()
|
||||
.expect("book_details_modal is empty")
|
||||
.book_details_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if EDITION_DETAILS_BLOCKS.contains(&active_readarr_block) => {
|
||||
Some(&EDITION_DETAILS_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveReadarrBlock::AddAuthorSearchInput
|
||||
| ActiveReadarrBlock::AddAuthorEmptySearchResults
|
||||
| ActiveReadarrBlock::TestAllIndexers
|
||||
| ActiveReadarrBlock::SystemLogs
|
||||
| ActiveReadarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
|
||||
_ if EDIT_AUTHOR_BLOCKS.contains(&active_readarr_block)
|
||||
|| EDIT_INDEXER_BLOCKS.contains(&active_readarr_block)
|
||||
|| INDEXER_SETTINGS_BLOCKS.contains(&active_readarr_block)
|
||||
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_readarr_block) =>
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
_ if DELETE_AUTHOR_BLOCKS.contains(&active_readarr_block)
|
||||
|| DELETE_BOOK_BLOCKS.contains(&active_readarr_block) =>
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveReadarrBlock::AddAuthorPrompt
|
||||
| ActiveReadarrBlock::AddAuthorSelectMonitor
|
||||
| ActiveReadarrBlock::AddAuthorSelectMonitorNewItems
|
||||
| ActiveReadarrBlock::AddAuthorSelectQualityProfile
|
||||
| ActiveReadarrBlock::AddAuthorSelectMetadataProfile
|
||||
| ActiveReadarrBlock::AddAuthorSelectRootFolder
|
||||
| ActiveReadarrBlock::AddAuthorTagsInput
|
||||
| ActiveReadarrBlock::AddAuthorAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
|
||||
_ if ADD_AUTHOR_BLOCKS.contains(&active_readarr_block) => {
|
||||
Some(&ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveReadarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
|
||||
_ => app
|
||||
.data
|
||||
.readarr_data
|
||||
.main_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::readarr::readarr_context_clues::{
|
||||
ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES, AUTHOR_DETAILS_CONTEXT_CLUES,
|
||||
AUTHOR_HISTORY_CONTEXT_CLUES, AUTHOR_OVERVIEW_CONTEXT_CLUES, AUTHORS_CONTEXT_CLUES,
|
||||
BOOK_DETAILS_CONTEXT_CLUES, BOOK_FILE_CONTEXT_CLUES, BOOK_HISTORY_CONTEXT_CLUES,
|
||||
EDITION_DETAILS_CONTEXT_CLUES, MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES,
|
||||
MANUAL_BOOK_SEARCH_CONTEXT_CLUES, ReadarrContextClueProvider,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::servarr_data::readarr::modals::BookDetailsModal;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ADD_ROOT_FOLDER_BLOCKS, ActiveReadarrBlock, EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
INDEXER_SETTINGS_BLOCKS, ReadarrData,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_authors_context_clues() {
|
||||
let mut authors_context_clues_iter = AUTHORS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, "update all")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
|
||||
);
|
||||
assert_none!(authors_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_context_clues() {
|
||||
let mut author_details_context_clues_iter = AUTHOR_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit author")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete book")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
"toggle book monitoring",
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.view, "view overview")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(author_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_context_clues() {
|
||||
let mut author_overview_context_clues_iter = AUTHOR_OVERVIEW_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
author_overview_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_overview_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(author_overview_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_search_results_context_clues() {
|
||||
let mut add_author_search_results_context_clues_iter =
|
||||
ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
add_author_search_results_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
add_author_search_results_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "edit search")
|
||||
);
|
||||
assert_none!(add_author_search_results_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "ReadarrContextClueProvider::get_context_clues called with non-Readarr route"
|
||||
)]
|
||||
fn test_readarr_context_clue_provider_get_context_clues_non_readarr_route() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_history_context_clues() {
|
||||
let mut author_history_context_clues_iter = AUTHOR_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit author")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
|
||||
);
|
||||
assert_none!(author_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_author_search_context_clues() {
|
||||
let mut manual_author_search_context_clues_iter = MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit author")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(manual_author_search_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_details_context_clues() {
|
||||
let mut book_details_context_clues_iter = BOOK_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "edition details")
|
||||
);
|
||||
assert_none!(book_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_history_context_clues() {
|
||||
let mut book_history_context_clues_iter = BOOK_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
|
||||
);
|
||||
assert_none!(book_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_file_context_clues() {
|
||||
let mut book_file_context_clues_iter = BOOK_FILE_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
book_file_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_file_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete book file")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_file_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(book_file_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_book_search_context_clues() {
|
||||
let mut manual_book_search_context_clues_iter = MANUAL_BOOK_SEARCH_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(manual_book_search_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edition_details_context_clues() {
|
||||
let mut edition_details_context_clues_iter = EDITION_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
edition_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
edition_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(edition_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveReadarrBlock::AuthorDetails, &AUTHOR_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveReadarrBlock::AuthorHistory, &AUTHOR_HISTORY_CONTEXT_CLUES)]
|
||||
#[case(2, ActiveReadarrBlock::ManualAuthorSearch, &MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES)]
|
||||
fn test_readarr_context_clue_provider_author_info_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_readarr_block: ActiveReadarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data = ReadarrData::default();
|
||||
app.data.readarr_data.author_info_tabs.set_index(index);
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveReadarrBlock::BookDetails, &BOOK_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveReadarrBlock::BookHistory, &BOOK_HISTORY_CONTEXT_CLUES)]
|
||||
#[case(2, ActiveReadarrBlock::BookFileInfo, &BOOK_FILE_CONTEXT_CLUES)]
|
||||
#[case(3, ActiveReadarrBlock::ManualBookSearch, &MANUAL_BOOK_SEARCH_CONTEXT_CLUES)]
|
||||
fn test_readarr_context_clue_provider_book_details_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_readarr_block: ActiveReadarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
let mut book_details_modal = BookDetailsModal::default();
|
||||
book_details_modal.book_details_tabs.set_index(index);
|
||||
let readarr_data = ReadarrData {
|
||||
book_details_modal: Some(book_details_modal),
|
||||
..ReadarrData::default()
|
||||
};
|
||||
app.data.readarr_data = readarr_data;
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_book_edition_details_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::BookEditionDetails.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &EDITION_DETAILS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_author_overview_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHOR_OVERVIEW_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_authors_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Authors.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_authors_sort_prompt_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorsSortPrompt.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_search_authors_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::SearchAuthors.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_filter_authors_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::FilterAuthors.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_readarr_context_clue_provider_bare_popup_context_clues(
|
||||
#[values(
|
||||
ActiveReadarrBlock::AddAuthorSearchInput,
|
||||
ActiveReadarrBlock::AddAuthorEmptySearchResults,
|
||||
ActiveReadarrBlock::TestAllIndexers,
|
||||
ActiveReadarrBlock::SystemLogs,
|
||||
ActiveReadarrBlock::SystemUpdates
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() {
|
||||
let mut blocks = EDIT_AUTHOR_BLOCKS.to_vec();
|
||||
blocks.extend(ADD_ROOT_FOLDER_BLOCKS);
|
||||
blocks.extend(INDEXER_SETTINGS_BLOCKS);
|
||||
blocks.extend(EDIT_INDEXER_BLOCKS);
|
||||
|
||||
for active_readarr_block in blocks {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_add_author_search_results_context_clues() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AddAuthorSearchResults.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_add_author_blocks(
|
||||
#[values(
|
||||
ActiveReadarrBlock::AddAuthorPrompt,
|
||||
ActiveReadarrBlock::AddAuthorSelectMonitor,
|
||||
ActiveReadarrBlock::AddAuthorSelectMonitorNewItems,
|
||||
ActiveReadarrBlock::AddAuthorSelectQualityProfile,
|
||||
ActiveReadarrBlock::AddAuthorSelectMetadataProfile,
|
||||
ActiveReadarrBlock::AddAuthorSelectRootFolder,
|
||||
ActiveReadarrBlock::AddAuthorTagsInput,
|
||||
ActiveReadarrBlock::AddAuthorAlreadyInLibrary
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_delete_author_blocks(
|
||||
#[values(
|
||||
ActiveReadarrBlock::DeleteAuthorPrompt,
|
||||
ActiveReadarrBlock::DeleteAuthorConfirmPrompt,
|
||||
ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion,
|
||||
ActiveReadarrBlock::DeleteAuthorToggleDeleteFile
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_delete_book_blocks(
|
||||
#[values(
|
||||
ActiveReadarrBlock::DeleteBookPrompt,
|
||||
ActiveReadarrBlock::DeleteBookConfirmPrompt,
|
||||
ActiveReadarrBlock::DeleteBookToggleAddListExclusion,
|
||||
ActiveReadarrBlock::DeleteBookToggleDeleteFile
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_system_tasks_clues() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
app.push_navigation_stack(ActiveReadarrBlock::SystemTasks.into());
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::models::readarr_models::{Author, Book, ReadarrRelease};
|
||||
use crate::models::servarr_data::readarr::modals::BookDetailsModal;
|
||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||
use crate::models::servarr_models::Indexer;
|
||||
use crate::network::NetworkEvent;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_authors() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::ListAuthors.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_author_details() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::AuthorDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetBooks(1).into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_blocklist_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Blocklist)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetBlocklist.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_author_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::AuthorHistory)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetAuthorHistory(1).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_author_search_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualAuthorSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetAuthorReleases(1).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_author_search_block_author_releases_non_empty() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_releases
|
||||
.set_items(vec![ReadarrRelease::default()]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualAuthorSearch)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_book_details_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
app.data.readarr_data.books.set_items(vec![Book {
|
||||
id: 2,
|
||||
..Book::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetBookEditions(2).into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetBookFiles(2).into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_book_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
app.data.readarr_data.books.set_items(vec![Book {
|
||||
id: 2,
|
||||
..Book::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookHistory)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetBookHistory(1, 2).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_book_history_block_no_op_when_books_table_is_empty() {
|
||||
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookHistory)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_book_search_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
app.data.readarr_data.books.set_items(vec![Book {
|
||||
id: 2,
|
||||
..Book::default()
|
||||
}]);
|
||||
app.data.readarr_data.book_details_modal = Some(BookDetailsModal::default());
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetBookReleases(2).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_book_search_block_is_loading() {
|
||||
let mut app = App {
|
||||
is_loading: true,
|
||||
..App::test_default()
|
||||
};
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_book_search_block_book_releases_non_empty() {
|
||||
let mut app = App::test_default();
|
||||
let mut book_details_modal = BookDetailsModal::default();
|
||||
book_details_modal
|
||||
.book_releases
|
||||
.set_items(vec![ReadarrRelease::default()]);
|
||||
app.data.readarr_data.book_details_modal = Some(book_details_modal);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_book_edition_details_block_does_not_dispatch() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.readarr_data.prompt_confirm = false;
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookEditionDetails)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_downloads_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Downloads)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_add_author_search_results() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.add_author_search = Some("test author".into());
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::AddAuthorSearchResults)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::SearchNewAuthor("test author".to_owned()).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::History)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetHistory(500).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_root_folders_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::RootFolders)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_indexers_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Indexers)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetIndexers.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_all_indexer_settings_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::AllIndexerSettingsPrompt)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetAllIndexerSettings.into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_test_indexer_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.indexers.set_items(vec![Indexer {
|
||||
id: 3,
|
||||
..Indexer::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::TestIndexer)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::TestIndexer(3).into()
|
||||
);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_test_all_indexers_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::TestAllIndexers)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::TestAllIndexers.into()
|
||||
);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_system_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::System)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTasks.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQueuedEvents.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetLogs(500).into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_system_updates_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::SystemUpdates)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetUpdates.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_resets_the_tick_count() {
|
||||
let mut app = App {
|
||||
tick_count: 2,
|
||||
..App::test_default()
|
||||
};
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
|
||||
.await;
|
||||
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_clears_the_pending_prompt_confirmation() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
|
||||
.await;
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_readarr_prompt_action_no_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = false;
|
||||
|
||||
app.check_for_readarr_prompt_action().await;
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_readarr_prompt_action_with_no_prompt_confirm_action() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
app.check_for_readarr_prompt_action().await;
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_none!(app.data.readarr_data.prompt_confirm_action);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_readarr_prompt_action() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::GetStatus);
|
||||
|
||||
app.check_for_readarr_prompt_action().await;
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
|
||||
assert!(app.should_refresh);
|
||||
assert_eq!(app.data.readarr_data.prompt_confirm_action, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_refresh_metadata() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
|
||||
app.refresh_readarr_metadata().await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetDiskSpace.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
|
||||
assert!(app.is_loading);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_first_render() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_first_render = true;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetDiskSpace.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert!(!app.is_first_render);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_routing() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
|
||||
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
app.should_refresh = false;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert!(app.cancellation_token.is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_should_refresh() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_should_refresh_does_not_cancel_prompt_requests() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_loading = true;
|
||||
app.is_routing = true;
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(app.should_refresh);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert!(!app.cancellation_token.is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_network_tick_frequency() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 2;
|
||||
app.tick_until_poll = 2;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_add_new_author_search_query() {
|
||||
let app = App::test_default_fully_populated();
|
||||
|
||||
let query = app.extract_add_new_author_search_query().await;
|
||||
|
||||
assert_str_eq!(query, "Test Author");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic(expected = "Add author search is empty")]
|
||||
async fn test_extract_add_new_author_search_query_panics_when_the_query_is_not_set() {
|
||||
let app = App::test_default();
|
||||
|
||||
app.extract_add_new_author_search_query().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_author_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
|
||||
assert_eq!(app.extract_author_id().await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_book_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.books.set_items(vec![Book {
|
||||
id: 2,
|
||||
..Book::default()
|
||||
}]);
|
||||
|
||||
assert_eq!(app.extract_book_id().await, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_readarr_indexer_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.indexers.set_items(vec![Indexer {
|
||||
id: 3,
|
||||
..Indexer::default()
|
||||
}]);
|
||||
|
||||
assert_eq!(app.extract_readarr_indexer_id().await, 3);
|
||||
}
|
||||
}
|
||||
@@ -58,21 +58,19 @@ impl App<'_> {
|
||||
}
|
||||
ActiveSonarrBlock::SeasonHistory => {
|
||||
if !self.data.sonarr_data.seasons.is_empty() {
|
||||
let (series_id, season_number) = self.extract_series_id_season_number_tuple().await;
|
||||
self
|
||||
.dispatch_network_event(
|
||||
SonarrEvent::GetSeasonHistory(self.extract_series_id_season_number_tuple().await)
|
||||
.into(),
|
||||
)
|
||||
.dispatch_network_event(SonarrEvent::GetSeasonHistory(series_id, season_number).into())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ActiveSonarrBlock::ManualSeasonSearch => {
|
||||
match self.data.sonarr_data.season_details_modal.as_ref() {
|
||||
Some(season_details_modal) if season_details_modal.season_releases.is_empty() => {
|
||||
let (series_id, season_number) = self.extract_series_id_season_number_tuple().await;
|
||||
self
|
||||
.dispatch_network_event(
|
||||
SonarrEvent::GetSeasonReleases(self.extract_series_id_season_number_tuple().await)
|
||||
.into(),
|
||||
SonarrEvent::GetSeasonReleases(series_id, season_number).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::{App, context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ADD_SERIES_BLOCKS, ActiveSonarrBlock, EDIT_INDEXER_BLOCKS, EDIT_SERIES_BLOCKS,
|
||||
EPISODE_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS,
|
||||
ADD_SERIES_BLOCKS, ActiveSonarrBlock, DELETE_SERIES_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
EDIT_SERIES_BLOCKS, EPISODE_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, SEASON_DETAILS_BLOCKS,
|
||||
SERIES_DETAILS_BLOCKS, SERIES_OVERVIEW_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -37,7 +39,7 @@ pub static SERIES_CONTEXT_CLUES: [ContextClue; 11] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
|
||||
pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
@@ -54,19 +56,16 @@ pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.view, "view overview"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
pub static SERIES_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
@@ -175,11 +174,6 @@ pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "start task"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub(in crate::app) struct SonarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for SonarrContextClueProvider {
|
||||
@@ -193,6 +187,9 @@ impl ContextClueProvider for SonarrContextClueProvider {
|
||||
.sonarr_data
|
||||
.series_info_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if SERIES_OVERVIEW_BLOCKS.contains(&active_sonarr_block) => {
|
||||
Some(&SERIES_OVERVIEW_CONTEXT_CLUES)
|
||||
}
|
||||
_ if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
|
||||
.data
|
||||
.sonarr_data
|
||||
@@ -223,6 +220,9 @@ impl ContextClueProvider for SonarrContextClueProvider {
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
_ if DELETE_SERIES_BLOCKS.contains(&active_sonarr_block) => {
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesPrompt
|
||||
| ActiveSonarrBlock::AddSeriesSelectMonitor
|
||||
| ActiveSonarrBlock::AddSeriesSelectSeriesType
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
mod tests {
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
|
||||
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
|
||||
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
|
||||
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::sonarr::sonarr_context_clues::{
|
||||
SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, SonarrContextClueProvider,
|
||||
@@ -13,10 +14,9 @@ mod tests {
|
||||
key_binding::DEFAULT_KEYBINDINGS,
|
||||
sonarr::sonarr_context_clues::{
|
||||
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
|
||||
HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
|
||||
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES,
|
||||
SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
|
||||
SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES,
|
||||
SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES,
|
||||
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SERIES_OVERVIEW_CONTEXT_CLUES,
|
||||
},
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
@@ -146,40 +146,6 @@ mod tests {
|
||||
assert_none!(series_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_context_clues() {
|
||||
let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
|
||||
);
|
||||
assert_none!(history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_series_details_context_clues() {
|
||||
let mut series_details_context_clues_iter = SERIES_DETAILS_CONTEXT_CLUES.iter();
|
||||
@@ -221,6 +187,10 @@ mod tests {
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
series_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.view, "view overview")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
series_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
@@ -228,6 +198,24 @@ mod tests {
|
||||
assert_none!(series_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_series_overview_context_clues() {
|
||||
let mut series_overview_context_clues_iter = SERIES_OVERVIEW_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
series_overview_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
series_overview_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(series_overview_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_context_clues() {
|
||||
let mut season_details_context_clues_iter = SEASON_DETAILS_CONTEXT_CLUES.iter();
|
||||
@@ -455,7 +443,6 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
// This should panic because the route is not a Sonarr route
|
||||
SonarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
@@ -477,6 +464,16 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_context_clue_provider_series_overview_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeriesOverview.into());
|
||||
|
||||
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &SERIES_OVERVIEW_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveSonarrBlock::SeasonDetails, &SEASON_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveSonarrBlock::SeasonHistory, &SEASON_HISTORY_CONTEXT_CLUES)]
|
||||
@@ -638,6 +635,24 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_delete_series_blocks(
|
||||
#[values(
|
||||
ActiveSonarrBlock::DeleteSeriesPrompt,
|
||||
ActiveSonarrBlock::DeleteSeriesConfirmPrompt,
|
||||
ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion,
|
||||
ActiveSonarrBlock::DeleteSeriesToggleDeleteFile
|
||||
)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
|
||||
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_sonarr_context_clue_provider_add_series_search_results_clues(
|
||||
#[values(
|
||||
|
||||
@@ -132,7 +132,7 @@ mod tests {
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
SonarrEvent::GetSeasonHistory((1, 1)).into()
|
||||
SonarrEvent::GetSeasonHistory(1, 1).into()
|
||||
);
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
@@ -175,7 +175,7 @@ mod tests {
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
SonarrEvent::GetSeasonReleases((1, 1)).into()
|
||||
SonarrEvent::GetSeasonReleases(1, 1).into()
|
||||
);
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
@@ -8,16 +8,28 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::lidarr::LidarrCommand;
|
||||
use crate::cli::readarr::ReadarrCommand;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{
|
||||
Cli,
|
||||
app::App,
|
||||
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{
|
||||
BlocklistItem as LidarrBlocklistItem, BlocklistResponse as LidarrBlocklistResponse,
|
||||
LidarrSerdeable,
|
||||
},
|
||||
radarr_models::{
|
||||
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
|
||||
RadarrSerdeable,
|
||||
},
|
||||
readarr_models::{
|
||||
BlocklistItem as ReadarrBlocklistItem, BlocklistResponse as ReadarrBlocklistResponse,
|
||||
ReadarrSerdeable,
|
||||
},
|
||||
sonarr_models::{
|
||||
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
|
||||
SonarrSerdeable,
|
||||
@@ -55,6 +67,20 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_subcommand_delegates_to_lidarr() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_subcommand_delegates_to_readarr() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "authors"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completions_requires_argument() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||
@@ -174,4 +200,66 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::BlocklistResponse(
|
||||
LidarrBlocklistResponse {
|
||||
records: vec![LidarrBlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let clear_blocklist_command = LidarrCommand::ClearBlocklist.into();
|
||||
|
||||
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cli_handler_delegates_readarr_commands_to_the_readarr_cli_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::BlocklistResponse(
|
||||
ReadarrBlocklistResponse {
|
||||
records: vec![ReadarrBlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let clear_blocklist_command = ReadarrCommand::ClearBlocklist.into();
|
||||
|
||||
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::LidarrCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::lidarr_models::{
|
||||
AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, MonitorType, NewItemMonitorType,
|
||||
},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "add_command_handler_tests.rs"]
|
||||
mod add_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrAddCommand {
|
||||
#[command(about = "Add a new artist to your Lidarr library")]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The MusicBrainz foreign artist ID of the artist you wish to add to your library",
|
||||
required = true
|
||||
)]
|
||||
foreign_artist_id: String,
|
||||
#[arg(long, help = "The name of the artist", required = true)]
|
||||
artist_name: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all artist data and metadata should live",
|
||||
required = true
|
||||
)]
|
||||
root_folder_path: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the quality profile to use for this artist",
|
||||
required = true
|
||||
)]
|
||||
quality_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the metadata profile to use for this artist",
|
||||
required = true
|
||||
)]
|
||||
metadata_profile_id: i64,
|
||||
#[arg(long, help = "Disable monitoring for this artist")]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag the artist with",
|
||||
value_parser,
|
||||
action = ArgAction::Append
|
||||
)]
|
||||
tag: Vec<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "What Lidarr should monitor for this artist",
|
||||
value_enum,
|
||||
default_value_t = MonitorType::default()
|
||||
)]
|
||||
monitor: MonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "How Lidarr should monitor new items for this artist",
|
||||
value_enum,
|
||||
default_value_t = NewItemMonitorType::default()
|
||||
)]
|
||||
monitor_new_items: NewItemMonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tell Lidarr to not start a search for missing albums once the artist is added to your library"
|
||||
)]
|
||||
no_search_for_missing_albums: bool,
|
||||
},
|
||||
#[command(about = "Add a new root folder")]
|
||||
RootFolder {
|
||||
#[arg(long, help = "The name of the root folder", required = true)]
|
||||
name: String,
|
||||
#[arg(long, help = "The path of the new root folder", required = true)]
|
||||
root_folder_path: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the default quality profile for artists in this root folder",
|
||||
required = true
|
||||
)]
|
||||
quality_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the default metadata profile for artists in this root folder",
|
||||
required = true
|
||||
)]
|
||||
metadata_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The default monitor option for artists in this root folder",
|
||||
value_enum,
|
||||
default_value_t = MonitorType::default()
|
||||
)]
|
||||
monitor: MonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The default monitor new items option for artists in this root folder",
|
||||
value_enum,
|
||||
default_value_t = NewItemMonitorType::default()
|
||||
)]
|
||||
monitor_new_items: NewItemMonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Default tag IDs for artists in this root folder",
|
||||
value_parser,
|
||||
action = ArgAction::Append
|
||||
)]
|
||||
tag: Vec<i64>,
|
||||
},
|
||||
#[command(about = "Add new tag")]
|
||||
Tag {
|
||||
#[arg(long, help = "The name of the tag to be added", required = true)]
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrAddCommand> for Command {
|
||||
fn from(value: LidarrAddCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Add(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrAddCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrAddCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrAddCommand::Artist {
|
||||
foreign_artist_id,
|
||||
artist_name,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
disable_monitoring,
|
||||
tag: tags,
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
no_search_for_missing_albums,
|
||||
} => {
|
||||
let body = AddArtistBody {
|
||||
foreign_artist_id,
|
||||
artist_name,
|
||||
monitored: !disable_monitoring,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
tags,
|
||||
tag_input_string: None,
|
||||
add_options: AddArtistOptions {
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
search_for_missing_albums: !no_search_for_missing_albums,
|
||||
},
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::AddArtist(body).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrAddCommand::RootFolder {
|
||||
name,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
tag: tags,
|
||||
} => {
|
||||
let add_root_folder_body = AddLidarrRootFolderBody {
|
||||
name,
|
||||
path: root_folder_path,
|
||||
default_quality_profile_id: quality_profile_id,
|
||||
default_metadata_profile_id: metadata_profile_id,
|
||||
default_monitor_option: monitor,
|
||||
default_new_item_monitor_option: monitor_new_items,
|
||||
default_tags: tags,
|
||||
tag_input_string: None,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::AddRootFolder(add_root_folder_body).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrAddCommand::Tag { name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::AddTag(name).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, add_command_handler::LidarrAddCommand},
|
||||
},
|
||||
models::lidarr_models::{MonitorType, NewItemMonitorType},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_add_command_from() {
|
||||
let command = LidarrAddCommand::Tag {
|
||||
name: String::new(),
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Add(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_add_root_folder_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "root-folder"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_root_folder_success() {
|
||||
let expected_args = LidarrAddCommand::RootFolder {
|
||||
name: "Music".to_owned(),
|
||||
root_folder_path: "/nfs/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
tag: vec![],
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"root-folder",
|
||||
"--name",
|
||||
"Music",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "tag"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_success() {
|
||||
let expected_args = LidarrAddCommand::Tag {
|
||||
name: "test".to_owned(),
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "lidarr", "add", "tag", "--name", "test"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_foreign_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--artist-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_artist_name() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_root_folder_path() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_quality_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_requires_metadata_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_success_with_required_args_only() {
|
||||
let expected_args = LidarrAddCommand::Artist {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
disable_monitoring: false,
|
||||
tag: vec![],
|
||||
monitor: MonitorType::default(),
|
||||
monitor_new_items: NewItemMonitorType::default(),
|
||||
no_search_for_missing_albums: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_success_with_all_args() {
|
||||
let expected_args = LidarrAddCommand::Artist {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 2,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::Future,
|
||||
monitor_new_items: NewItemMonitorType::New,
|
||||
no_search_for_missing_albums: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--disable-monitoring",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
"--monitor",
|
||||
"future",
|
||||
"--monitor-new-items",
|
||||
"new",
|
||||
"--no-search-for-missing-albums",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_monitor_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--monitor",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_new_item_monitor_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--monitor-new-items",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_tags_is_repeatable() {
|
||||
let expected_args = LidarrAddCommand::Artist {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 2,
|
||||
disable_monitoring: false,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::default(),
|
||||
monitor_new_items: NewItemMonitorType::default(),
|
||||
no_search_for_missing_albums: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"add",
|
||||
"artist",
|
||||
"--foreign-artist-id",
|
||||
"test-id",
|
||||
"--artist-name",
|
||||
"Test Artist",
|
||||
"--root-folder-path",
|
||||
"/music",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::lidarr::add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::{
|
||||
AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, LidarrSerdeable, MonitorType,
|
||||
NewItemMonitorType,
|
||||
};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_root_folder_command() {
|
||||
let expected_root_folder_path = "/nfs/test".to_owned();
|
||||
let expected_add_root_folder_body = AddLidarrRootFolderBody {
|
||||
name: "Music".to_owned(),
|
||||
path: expected_root_folder_path.clone(),
|
||||
default_quality_profile_id: 1,
|
||||
default_metadata_profile_id: 1,
|
||||
default_monitor_option: MonitorType::All,
|
||||
default_new_item_monitor_option: NewItemMonitorType::All,
|
||||
default_tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_root_folder_command = LidarrAddCommand::RootFolder {
|
||||
name: "Music".to_owned(),
|
||||
root_folder_path: expected_root_folder_path,
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
tag: vec![1, 2],
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_tag_command() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_tag_command = LidarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
};
|
||||
|
||||
let result = LidarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_artist_command() {
|
||||
let expected_body = AddArtistBody {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
monitored: false,
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
add_options: AddArtistOptions {
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
search_for_missing_albums: false,
|
||||
},
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::AddArtist(expected_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_artist_command = LidarrAddCommand::Artist {
|
||||
foreign_artist_id: "test-id".to_owned(),
|
||||
artist_name: "Test Artist".to_owned(),
|
||||
root_folder_path: "/music".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
no_search_for_missing_albums: true,
|
||||
};
|
||||
|
||||
let result = LidarrAddCommandHandler::with(&app_arc, add_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::lidarr_models::DeleteParams,
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_command_handler_tests.rs"]
|
||||
mod delete_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrDeleteCommand {
|
||||
#[command(about = "Delete an album from your Lidarr library")]
|
||||
Album {
|
||||
#[arg(long, help = "The ID of the album to delete", required = true)]
|
||||
album_id: i64,
|
||||
#[arg(long, help = "Delete the album files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this album")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the specified item from the Lidarr blocklist")]
|
||||
BlocklistItem {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the blocklist item to remove from the blocklist",
|
||||
required = true
|
||||
)]
|
||||
blocklist_item_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the specified track file from disk")]
|
||||
TrackFile {
|
||||
#[arg(long, help = "The ID of the track file to delete", required = true)]
|
||||
track_file_id: i64,
|
||||
},
|
||||
#[command(about = "Delete an artist from your Lidarr library")]
|
||||
Artist {
|
||||
#[arg(long, help = "The ID of the artist to delete", required = true)]
|
||||
artist_id: i64,
|
||||
#[arg(long, help = "Delete the artist files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this artist")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the specified download")]
|
||||
Download {
|
||||
#[arg(long, help = "The ID of the download to delete", required = true)]
|
||||
download_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the indexer with the given ID")]
|
||||
Indexer {
|
||||
#[arg(long, help = "The ID of the indexer to delete", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the root folder with the given ID")]
|
||||
RootFolder {
|
||||
#[arg(long, help = "The ID of the root folder to delete", required = true)]
|
||||
root_folder_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the tag with the specified ID")]
|
||||
Tag {
|
||||
#[arg(long, help = "The ID of the tag to delete", required = true)]
|
||||
tag_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrDeleteCommand> for Command {
|
||||
fn from(value: LidarrDeleteCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Delete(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrDeleteCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrDeleteCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrDeleteCommand::Album {
|
||||
album_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_album_params = DeleteParams {
|
||||
id: album_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteAlbum(delete_album_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteBlocklistItem(blocklist_item_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::TrackFile { track_file_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteTrackFile(track_file_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::Artist {
|
||||
artist_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_artist_params = DeleteParams {
|
||||
id: artist_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteArtist(delete_artist_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::Download { download_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteDownload(download_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::Indexer { indexer_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteIndexer(indexer_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::RootFolder { root_folder_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteRootFolder(root_folder_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrDeleteCommand::Tag { tag_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DeleteTag(tag_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, delete_command_handler::LidarrDeleteCommand},
|
||||
},
|
||||
};
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_delete_command_from() {
|
||||
let command = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Delete(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "album"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_defaults() {
|
||||
let expected_args = LidarrDeleteCommand::Album {
|
||||
album_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "lidarr", "delete", "album", "--album-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_album_all_args_defined() {
|
||||
let expected_args = LidarrDeleteCommand::Album {
|
||||
album_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"album",
|
||||
"--album-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "blocklist-item"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_success() {
|
||||
let expected_args = LidarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 1,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"blocklist-item",
|
||||
"--blocklist-item-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_track_file_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "track-file"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_track_file_success() {
|
||||
let expected_args = LidarrDeleteCommand::TrackFile { track_file_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"track-file",
|
||||
"--track-file-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_defaults() {
|
||||
let expected_args = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "lidarr", "delete", "artist", "--artist-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_artist_all_args_defined() {
|
||||
let expected_args = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "download"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_success() {
|
||||
let expected_args = LidarrDeleteCommand::Download { download_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"download",
|
||||
"--download-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "indexer"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_success() {
|
||||
let expected_args = LidarrDeleteCommand::Indexer { indexer_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "root-folder"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_success() {
|
||||
let expected_args = LidarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"delete",
|
||||
"root-folder",
|
||||
"--root-folder-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_success() {
|
||||
let expected_args = LidarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "lidarr", "delete", "tag", "--tag-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
lidarr::delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler},
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{DeleteParams, LidarrSerdeable},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_album_command() {
|
||||
let expected_delete_album_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteAlbum(expected_delete_album_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_album_command = LidarrDeleteCommand::Album {
|
||||
album_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_album_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_blocklist_item_command() {
|
||||
let expected_blocklist_item_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_blocklist_item_command = LidarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 1,
|
||||
};
|
||||
|
||||
let result = LidarrDeleteCommandHandler::with(
|
||||
&app_arc,
|
||||
delete_blocklist_item_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_track_file_command() {
|
||||
let expected_track_file_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteTrackFile(expected_track_file_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_track_file_command = LidarrDeleteCommand::TrackFile { track_file_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_track_file_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_artist_command() {
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteArtist(expected_delete_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_artist_command = LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_download_command() {
|
||||
let expected_download_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteDownload(expected_download_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_download_command = LidarrDeleteCommand::Download { download_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_indexer_command() {
|
||||
let expected_indexer_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteIndexer(expected_indexer_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_indexer_command = LidarrDeleteCommand::Indexer { indexer_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_root_folder_command() {
|
||||
let expected_root_folder_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteRootFolder(expected_root_folder_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_root_folder_command = LidarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_tag_command() {
|
||||
let expected_tag_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteTag(expected_tag_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_tag_command = LidarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, ArgGroup, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::LidarrCommand;
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::LidarrSerdeable;
|
||||
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command, mutex_flags_or_option},
|
||||
models::lidarr_models::{EditArtistParams, NewItemMonitorType},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_command_handler_tests.rs"]
|
||||
mod edit_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrEditCommand {
|
||||
#[command(
|
||||
about = "Edit and indexer settings that apply to all indexers",
|
||||
group(
|
||||
ArgGroup::new("edit_settings")
|
||||
.args([
|
||||
"maximum_size",
|
||||
"minimum_age",
|
||||
"retention",
|
||||
"rss_sync_interval",
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
AllIndexerSettings {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
|
||||
)]
|
||||
maximum_size: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
|
||||
)]
|
||||
minimum_age: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
|
||||
)]
|
||||
retention: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
|
||||
)]
|
||||
rss_sync_interval: Option<i64>,
|
||||
},
|
||||
#[command(
|
||||
about = "Edit preferences for the specified artist",
|
||||
group(
|
||||
ArgGroup::new("edit_artist")
|
||||
.args([
|
||||
"enable_monitoring",
|
||||
"disable_monitoring",
|
||||
"monitor_new_items",
|
||||
"quality_profile_id",
|
||||
"metadata_profile_id",
|
||||
"root_folder_path",
|
||||
"tag",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the artist whose settings you want to edit",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Enable monitoring of this artist in Lidarr so Lidarr will automatically download releases from this artist if they are available",
|
||||
conflicts_with = "disable_monitoring"
|
||||
)]
|
||||
enable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable monitoring of this artist so Lidarr does not automatically download releases from this artist if they are available",
|
||||
conflicts_with = "enable_monitoring"
|
||||
)]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "How Lidarr should monitor new albums from this artist",
|
||||
value_enum
|
||||
)]
|
||||
monitor_new_items: Option<NewItemMonitorType>,
|
||||
#[arg(long, help = "The ID of the quality profile to use for this artist")]
|
||||
quality_profile_id: Option<i64>,
|
||||
#[arg(long, help = "The ID of the metadata profile to use for this artist")]
|
||||
metadata_profile_id: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all artist data and metadata should live"
|
||||
)]
|
||||
root_folder_path: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag this artist with",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(long, help = "Clear all tags on this artist", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
#[command(
|
||||
about = "Edit preferences for the specified indexer",
|
||||
group(
|
||||
ArgGroup::new("edit_indexer")
|
||||
.args([
|
||||
"name",
|
||||
"enable_rss",
|
||||
"disable_rss",
|
||||
"enable_automatic_search",
|
||||
"disable_automatic_search",
|
||||
"enable_interactive_search",
|
||||
"disable_automatic_search",
|
||||
"url",
|
||||
"api_key",
|
||||
"seed_ratio",
|
||||
"tag",
|
||||
"priority",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Indexer {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the indexer whose settings you wish to edit",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
#[arg(long, help = "The name of the indexer")]
|
||||
name: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Lidarr that this indexer should be used when Lidarr periodically looks for releases via RSS Sync",
|
||||
conflicts_with = "disable_rss"
|
||||
)]
|
||||
enable_rss: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer when Lidarr periodically looks for releases via RSS Sync",
|
||||
conflicts_with = "enable_rss"
|
||||
)]
|
||||
disable_rss: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Lidarr that this indexer should be used when automatic searches are performed via the UI or by Lidarr",
|
||||
conflicts_with = "disable_automatic_search"
|
||||
)]
|
||||
enable_automatic_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Lidarr",
|
||||
conflicts_with = "enable_automatic_search"
|
||||
)]
|
||||
disable_automatic_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Lidarr that this indexer should be used when an interactive search is used",
|
||||
conflicts_with = "disable_interactive_search"
|
||||
)]
|
||||
enable_interactive_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer whenever an interactive search is performed",
|
||||
conflicts_with = "enable_interactive_search"
|
||||
)]
|
||||
disable_interactive_search: bool,
|
||||
#[arg(long, help = "The URL of the indexer")]
|
||||
url: Option<String>,
|
||||
#[arg(long, help = "The API key used to access the indexer's API")]
|
||||
api_key: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
|
||||
)]
|
||||
seed_ratio: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Only use this indexer for series with at least one matching tag ID. Leave blank to use with all series.",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Lidarr will still use all enabled indexers for RSS Sync and Searching"
|
||||
)]
|
||||
priority: Option<i64>,
|
||||
#[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrEditCommand> for Command {
|
||||
fn from(value: LidarrEditCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Edit(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrEditCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrEditCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrEditCommand::AllIndexerSettings {
|
||||
maximum_size,
|
||||
minimum_age,
|
||||
retention,
|
||||
rss_sync_interval,
|
||||
} => {
|
||||
if let Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(previous_indexer_settings)) = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAllIndexerSettings.into())
|
||||
.await?
|
||||
{
|
||||
let params = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
|
||||
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
|
||||
retention: retention.unwrap_or(previous_indexer_settings.retention),
|
||||
rss_sync_interval: rss_sync_interval
|
||||
.unwrap_or(previous_indexer_settings.rss_sync_interval),
|
||||
};
|
||||
self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::EditAllIndexerSettings(params).into())
|
||||
.await?;
|
||||
"All indexer settings updated".to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
LidarrEditCommand::Artist {
|
||||
artist_id,
|
||||
enable_monitoring,
|
||||
disable_monitoring,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tag,
|
||||
clear_tags,
|
||||
} => {
|
||||
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
|
||||
let edit_artist_params = EditArtistParams {
|
||||
artist_id,
|
||||
monitored: monitored_value,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tags: tag,
|
||||
tag_input_string: None,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::EditArtist(edit_artist_params).into())
|
||||
.await?;
|
||||
"Artist Updated".to_owned()
|
||||
}
|
||||
LidarrEditCommand::Indexer {
|
||||
indexer_id,
|
||||
name,
|
||||
enable_rss,
|
||||
disable_rss,
|
||||
enable_automatic_search,
|
||||
disable_automatic_search,
|
||||
enable_interactive_search,
|
||||
disable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tag,
|
||||
priority,
|
||||
clear_tags,
|
||||
} => {
|
||||
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
|
||||
let automatic_search_value =
|
||||
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
|
||||
let interactive_search_value =
|
||||
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
|
||||
let edit_indexer_params = EditIndexerParams {
|
||||
indexer_id,
|
||||
name,
|
||||
enable_rss: rss_value,
|
||||
enable_automatic_search: automatic_search_value,
|
||||
enable_interactive_search: interactive_search_value,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tags: tag,
|
||||
tag_input_string: None,
|
||||
priority,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::EditIndexer(edit_indexer_params).into())
|
||||
.await?;
|
||||
"Indexer updated".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,858 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, edit_command_handler::LidarrEditCommand},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_edit_command_from() {
|
||||
let command = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: None,
|
||||
quality_profile_id: None,
|
||||
metadata_profile_id: None,
|
||||
root_folder_path: None,
|
||||
tag: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Edit(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use crate::{Cli, models::lidarr_models::NewItemMonitorType};
|
||||
|
||||
use super::*;
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_edit_all_indexer_settings_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "all-indexer-settings"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_all_indexer_settings_assert_argument_flags_require_args(
|
||||
#[values(
|
||||
"--maximum-size",
|
||||
"--minimum-age",
|
||||
"--retention",
|
||||
"--rss-sync-interval"
|
||||
)]
|
||||
flag: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"all-indexer-settings",
|
||||
flag,
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() {
|
||||
let expected_args = LidarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: None,
|
||||
retention: None,
|
||||
rss_sync_interval: None,
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"all-indexer-settings",
|
||||
"--maximum-size",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_all_indexer_settings_all_arguments_defined() {
|
||||
let expected_args = LidarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"all-indexer-settings",
|
||||
"--maximum-size",
|
||||
"1",
|
||||
"--minimum-age",
|
||||
"1",
|
||||
"--retention",
|
||||
"1",
|
||||
"--rss-sync-interval",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_with_artist_id_still_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_monitoring_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--enable-monitoring",
|
||||
"--disable-monitoring",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_tag_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--clear-tags",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_artist_assert_argument_flags_require_args(
|
||||
#[values(
|
||||
"--monitor-new-items",
|
||||
"--quality-profile-id",
|
||||
"--metadata-profile-id",
|
||||
"--root-folder-path",
|
||||
"--tag"
|
||||
)]
|
||||
flag: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
flag,
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_monitor_new_items_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--monitor-new-items",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_only_requires_at_least_one_argument_plus_artist_id() {
|
||||
let expected_args = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: None,
|
||||
quality_profile_id: None,
|
||||
metadata_profile_id: None,
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_tag_argument_is_repeatable() {
|
||||
let expected_args = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: None,
|
||||
quality_profile_id: None,
|
||||
metadata_profile_id: None,
|
||||
root_folder_path: None,
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_artist_all_arguments_defined() {
|
||||
let expected_args = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: true,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: Some(NewItemMonitorType::New),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--enable-monitoring",
|
||||
"--monitor-new-items",
|
||||
"new",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "indexer"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_with_indexer_id_still_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_rss_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--enable-rss",
|
||||
"--disable-rss",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_automatic_search_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--enable-automatic-search",
|
||||
"--disable-automatic-search",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_interactive_search_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--enable-interactive-search",
|
||||
"--disable-interactive-search",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_tag_flags_conflict() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--clear-tags",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_assert_argument_flags_require_args(
|
||||
#[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
flag,
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() {
|
||||
let expected_args = LidarrEditCommand::Indexer {
|
||||
indexer_id: 1,
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: false,
|
||||
disable_rss: false,
|
||||
enable_automatic_search: false,
|
||||
disable_automatic_search: false,
|
||||
enable_interactive_search: false,
|
||||
disable_interactive_search: false,
|
||||
url: None,
|
||||
api_key: None,
|
||||
seed_ratio: None,
|
||||
tag: None,
|
||||
priority: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--name",
|
||||
"Test",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_tag_argument_is_repeatable() {
|
||||
let expected_args = LidarrEditCommand::Indexer {
|
||||
indexer_id: 1,
|
||||
name: None,
|
||||
enable_rss: false,
|
||||
disable_rss: false,
|
||||
enable_automatic_search: false,
|
||||
disable_automatic_search: false,
|
||||
enable_interactive_search: false,
|
||||
disable_interactive_search: false,
|
||||
url: None,
|
||||
api_key: None,
|
||||
seed_ratio: None,
|
||||
tag: Some(vec![1, 2]),
|
||||
priority: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_all_arguments_defined() {
|
||||
let expected_args = LidarrEditCommand::Indexer {
|
||||
indexer_id: 1,
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: true,
|
||||
disable_rss: false,
|
||||
enable_automatic_search: true,
|
||||
disable_automatic_search: false,
|
||||
enable_interactive_search: true,
|
||||
disable_interactive_search: false,
|
||||
url: Some("http://test.com".to_owned()),
|
||||
api_key: Some("testKey".to_owned()),
|
||||
seed_ratio: Some("1.2".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
priority: Some(25),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"edit",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
"--name",
|
||||
"Test",
|
||||
"--enable-rss",
|
||||
"--enable-automatic-search",
|
||||
"--enable-interactive-search",
|
||||
"--url",
|
||||
"http://test.com",
|
||||
"--api-key",
|
||||
"testKey",
|
||||
"--seed-ratio",
|
||||
"1.2",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
"--priority",
|
||||
"25",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(edit_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
lidarr::edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler},
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{EditArtistParams, LidarrSerdeable, NewItemMonitorType},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_all_indexer_settings_command() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 12345,
|
||||
minimum_age: 11,
|
||||
retention: 29,
|
||||
rss_sync_interval: 60,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 5,
|
||||
maximum_size: 22222,
|
||||
minimum_age: 34,
|
||||
retention: 48,
|
||||
rss_sync_interval: 90,
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_all_indexer_settings_command = LidarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(12345),
|
||||
minimum_age: Some(11),
|
||||
retention: Some(29),
|
||||
rss_sync_interval: Some(60),
|
||||
};
|
||||
|
||||
let result = LidarrEditCommandHandler::with(
|
||||
&app_arc,
|
||||
edit_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_artist_command() {
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 1,
|
||||
monitored: Some(true),
|
||||
monitor_new_items: Some(NewItemMonitorType::New),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
tag_input_string: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_artist_command = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: true,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: Some(NewItemMonitorType::New),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_artist_command_handles_disable_monitoring_flag_properly() {
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 1,
|
||||
monitored: Some(false),
|
||||
monitor_new_items: Some(NewItemMonitorType::None),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
tag_input_string: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_artist_command = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: true,
|
||||
monitor_new_items: Some(NewItemMonitorType::None),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_artist_command_no_monitoring_boolean_flags_returns_none_value() {
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 1,
|
||||
monitored: None,
|
||||
monitor_new_items: Some(NewItemMonitorType::All),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
tag_input_string: None,
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditArtist(expected_edit_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_artist_command = LidarrEditCommand::Artist {
|
||||
artist_id: 1,
|
||||
enable_monitoring: false,
|
||||
disable_monitoring: false,
|
||||
monitor_new_items: Some(NewItemMonitorType::All),
|
||||
quality_profile_id: Some(1),
|
||||
metadata_profile_id: Some(1),
|
||||
root_folder_path: Some("/nfs/test".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_indexer_command() {
|
||||
let expected_edit_indexer_params = EditIndexerParams {
|
||||
indexer_id: 1,
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: Some(true),
|
||||
enable_automatic_search: Some(true),
|
||||
enable_interactive_search: Some(true),
|
||||
url: Some("http://test.com".to_owned()),
|
||||
api_key: Some("testKey".to_owned()),
|
||||
seed_ratio: Some("1.2".to_owned()),
|
||||
tags: Some(vec![1, 2]),
|
||||
tag_input_string: None,
|
||||
priority: Some(25),
|
||||
clear_tags: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditIndexer(expected_edit_indexer_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_indexer_command = LidarrEditCommand::Indexer {
|
||||
indexer_id: 1,
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: true,
|
||||
disable_rss: false,
|
||||
enable_automatic_search: true,
|
||||
disable_automatic_search: false,
|
||||
enable_interactive_search: true,
|
||||
disable_interactive_search: false,
|
||||
url: Some("http://test.com".to_owned()),
|
||||
api_key: Some("testKey".to_owned()),
|
||||
seed_ratio: Some("1.2".to_owned()),
|
||||
tag: Some(vec![1, 2]),
|
||||
priority: Some(25),
|
||||
clear_tags: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "get_command_handler_tests.rs"]
|
||||
mod get_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrGetCommand {
|
||||
#[command(about = "Get detailed information for the album with the given ID")]
|
||||
AlbumDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the album whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(about = "Get the shared settings for all indexers")]
|
||||
AllIndexerSettings,
|
||||
#[command(about = "Get detailed information for the artist with the given ID")]
|
||||
ArtistDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch the host config for your Lidarr instance")]
|
||||
HostConfig,
|
||||
#[command(about = "Fetch the security config for your Lidarr instance")]
|
||||
SecurityConfig,
|
||||
#[command(about = "Get the system status")]
|
||||
SystemStatus,
|
||||
#[command(about = "Get detailed information for the track with the given ID")]
|
||||
TrackDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the track whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
track_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrGetCommand> for Command {
|
||||
fn from(value: LidarrGetCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Get(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrGetCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrGetCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrGetCommand::AlbumDetails { album_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAlbumDetails(album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::AllIndexerSettings => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAllIndexerSettings.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::ArtistDetails { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetArtistDetails(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::HostConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetHostConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::SecurityConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetSecurityConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::SystemStatus => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetStatus.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrGetCommand::TrackDetails { track_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTrackDetails(track_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, get_command_handler::LidarrGetCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_get_command_from() {
|
||||
let command = LidarrGetCommand::SystemStatus;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Get(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_album_details_requires_album_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "album-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"get",
|
||||
"album-details",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_indexer_settings_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "all-indexer-settings"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_requires_artist_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "artist-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"get",
|
||||
"artist-details",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_host_config_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "host-config"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_config_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "security-config"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_status_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "system-status"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_requires_track_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "track-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"get",
|
||||
"track-details",
|
||||
"--track-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
lidarr::get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler},
|
||||
},
|
||||
models::{Serdeable, lidarr_models::LidarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_album_details_command() {
|
||||
let expected_album_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetAlbumDetails(expected_album_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_album_details_command = LidarrGetCommand::AlbumDetails { album_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_album_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_all_indexer_settings_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_all_indexer_settings_command = LidarrGetCommand::AllIndexerSettings;
|
||||
|
||||
let result = LidarrGetCommandHandler::with(
|
||||
&app_arc,
|
||||
get_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_artist_details_command() {
|
||||
let expected_artist_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetArtistDetails(expected_artist_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_artist_details_command = LidarrGetCommand::ArtistDetails { artist_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_artist_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_host_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetHostConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_host_config_command = LidarrGetCommand::HostConfig;
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_security_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetSecurityConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_security_config_command = LidarrGetCommand::SecurityConfig;
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_system_status_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_system_status_command = LidarrGetCommand::SystemStatus;
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_track_details_command() {
|
||||
let expected_track_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetTrackDetails(expected_track_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_track_details_command = LidarrGetCommand::TrackDetails { track_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrGetCommandHandler::with(&app_arc, get_track_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,773 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, list_command_handler::LidarrListCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_command_from() {
|
||||
let command = LidarrCommand::List(LidarrListCommand::Artists);
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(command));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_commands_that_have_no_arg_requirements(
|
||||
#[values("clear-blocklist", "test-all-indexers")] subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_artists_has_no_arg_requirements() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_list_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_add_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_delete_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requires_guid() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"download-release",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"download-release",
|
||||
"--guid",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"download-release",
|
||||
"--guid",
|
||||
"1",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_artist_monitoring_requires_artist_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-artist-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_artist_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"toggle-artist-monitoring",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_album_monitoring_requires_album_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-album-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_album_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"toggle-album-monitoring",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_artist_requires_query() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "search-new-artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_artist_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"search-new-artist",
|
||||
"--query",
|
||||
"test query",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_requires_task_name() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "start-task"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_task_name_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"start-task",
|
||||
"--task-name",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"start-task",
|
||||
"--task-name",
|
||||
"application-update-check",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requires_history_item_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "mark-history-item-as-failed"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"mark-history-item-as-failed",
|
||||
"--history-item-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_indexer_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "test-indexer"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_indexer_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"test-indexer",
|
||||
"--indexer-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::lidarr::add_command_handler::LidarrAddCommand;
|
||||
use crate::cli::lidarr::edit_command_handler::LidarrEditCommand;
|
||||
use crate::cli::lidarr::get_command_handler::LidarrGetCommand;
|
||||
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
|
||||
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
|
||||
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
|
||||
use crate::models::lidarr_models::{BlocklistItem, BlocklistResponse, LidarrTaskName};
|
||||
use crate::models::servarr_models::{IndexerSettings, ReleaseDownloadBody};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
lidarr::{
|
||||
LidarrCliHandler, LidarrCommand, delete_command_handler::LidarrDeleteCommand,
|
||||
list_command_handler::LidarrListCommand,
|
||||
},
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
lidarr_models::{Artist, DeleteParams, LidarrSerdeable},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_tag_command = LidarrCommand::Add(LidarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_system_status_command = LidarrCommand::Get(LidarrGetCommand::SystemStatus);
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DeleteArtist(expected_delete_artist_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_artist_command = LidarrCommand::Delete(LidarrDeleteCommand::Artist {
|
||||
artist_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, delete_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_all_indexer_settings_command =
|
||||
LidarrCommand::Edit(LidarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(
|
||||
&app_arc,
|
||||
edit_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::ListArtists.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Artists(vec![
|
||||
Artist::default(),
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_artists_command = LidarrCommand::List(LidarrListCommand::Artists);
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, list_artists_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::UpdateAllArtists.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_artist_command = LidarrCommand::Refresh(LidarrRefreshCommand::AllArtists);
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, refresh_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler()
|
||||
{
|
||||
let expected_artist_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_episode_search_command =
|
||||
LidarrCommand::ManualSearch(LidarrManualSearchCommand::Discography { artist_id: 1 });
|
||||
|
||||
let result =
|
||||
LidarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
|
||||
{
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist {
|
||||
artist_id: 1,
|
||||
});
|
||||
|
||||
let result = LidarrCliHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_clear_blocklist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::BlocklistResponse(
|
||||
BlocklistResponse {
|
||||
records: vec![BlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let claer_blocklist_command = LidarrCommand::ClearBlocklist;
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_release_command() {
|
||||
let expected_release_download_body = ReleaseDownloadBody {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let download_release_command = LidarrCommand::DownloadRelease {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
};
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_toggle_artist_monitoring_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::ToggleArtistMonitoring(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let toggle_artist_monitoring_command = LidarrCommand::ToggleArtistMonitoring { artist_id: 1 };
|
||||
|
||||
let result = LidarrCliHandler::with(
|
||||
&app_arc,
|
||||
toggle_artist_monitoring_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_new_artist_command() {
|
||||
let expected_query = "test artist".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::SearchNewArtist(expected_query.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let search_new_artist_command = LidarrCommand::SearchNewArtist {
|
||||
query: expected_query,
|
||||
};
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, search_new_artist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_start_task_command() {
|
||||
let expected_task_name = LidarrTaskName::ApplicationUpdateCheck;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::StartTask(expected_task_name).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let start_task_command = LidarrCommand::StartTask {
|
||||
task_name: LidarrTaskName::ApplicationUpdateCheck,
|
||||
};
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_test_indexer_command() {
|
||||
let expected_indexer_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::TestIndexer(expected_indexer_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let test_indexer_command = LidarrCommand::TestIndexer { indexer_id: 1 };
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_test_all_indexers_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::TestAllIndexers.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let test_all_indexers_command = LidarrCommand::TestAllIndexers;
|
||||
|
||||
let result = LidarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_history_item_as_failed_command() {
|
||||
let expected_history_item_id = 1i64;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let mark_history_item_as_failed_command = LidarrCommand::MarkHistoryItemAsFailed {
|
||||
history_item_id: expected_history_item_id,
|
||||
};
|
||||
|
||||
let result = LidarrCliHandler::with(
|
||||
&app_arc,
|
||||
mark_history_item_as_failed_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::LidarrCommand;
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "list_command_handler_tests.rs"]
|
||||
mod list_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrListCommand {
|
||||
#[command(about = "List all albums for the artist with the given ID")]
|
||||
Albums {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist whose albums you want to list",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Fetch all history events for the given album corresponding to the artist with the given ID."
|
||||
)]
|
||||
AlbumHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr artist ID of the artist whose history you wish to fetch and list",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr album ID to fetch history events for",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch all history events for the artist with the given ID")]
|
||||
ArtistHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
#[command(about = "List all artists in your Lidarr library")]
|
||||
Artists,
|
||||
#[command(about = "List all items in the Lidarr blocklist")]
|
||||
Blocklist,
|
||||
#[command(about = "List disk space details for all provisioned root folders in Lidarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "List all active downloads in Lidarr")]
|
||||
Downloads {
|
||||
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
|
||||
count: u64,
|
||||
},
|
||||
#[command(about = "Fetch all Lidarr history events")]
|
||||
History {
|
||||
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
},
|
||||
#[command(about = "List all Lidarr indexers")]
|
||||
Indexers,
|
||||
#[command(about = "Fetch Lidarr logs")]
|
||||
Logs {
|
||||
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Output the logs in the same format as they appear in the log files"
|
||||
)]
|
||||
output_in_log_format: bool,
|
||||
},
|
||||
#[command(about = "List all Lidarr metadata profiles")]
|
||||
MetadataProfiles,
|
||||
#[command(about = "List all Lidarr quality profiles")]
|
||||
QualityProfiles,
|
||||
#[command(about = "List all queued events")]
|
||||
QueuedEvents,
|
||||
#[command(about = "List all root folders in Lidarr")]
|
||||
RootFolders,
|
||||
#[command(about = "List all Lidarr tags")]
|
||||
Tags,
|
||||
#[command(about = "List all Lidarr tasks")]
|
||||
Tasks,
|
||||
#[command(about = "Fetch all history events for the track with the given ID")]
|
||||
TrackHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The artist ID that the track belongs to",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The album ID that the track is a part of",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the track whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
track_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "List the tracks for the album that corresponds to the artist with the given ID"
|
||||
)]
|
||||
Tracks {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr artist ID of the artist whose tracks you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr album ID whose tracks you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(about = "List the track files for the album with the given ID")]
|
||||
TrackFiles {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the album whose track files you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(about = "List all Lidarr updates")]
|
||||
Updates,
|
||||
}
|
||||
|
||||
impl From<LidarrListCommand> for Command {
|
||||
fn from(value: LidarrListCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::List(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrListCommandHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrListCommandHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrListCommand::Albums { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAlbums(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::AlbumHistory {
|
||||
artist_id,
|
||||
album_id,
|
||||
} => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAlbumHistory(artist_id, album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::ArtistHistory { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetArtistHistory(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Artists => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ListArtists.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Blocklist => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::DiskSpace => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetDiskSpace.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Downloads { count } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetDownloads(count).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::History { events: items } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetHistory(items).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Indexers => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Logs {
|
||||
events,
|
||||
output_in_log_format,
|
||||
} => {
|
||||
let logs = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetLogs(events).into())
|
||||
.await?;
|
||||
|
||||
if output_in_log_format {
|
||||
let log_lines = &self.app.lock().await.data.sonarr_data.logs.items;
|
||||
|
||||
serde_json::to_string_pretty(log_lines)?
|
||||
} else {
|
||||
serde_json::to_string_pretty(&logs)?
|
||||
}
|
||||
}
|
||||
LidarrListCommand::MetadataProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetMetadataProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::QualityProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetQualityProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::QueuedEvents => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetQueuedEvents.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::RootFolders => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetRootFolders.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Tags => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTags.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Tasks => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTasks.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::TrackHistory {
|
||||
artist_id,
|
||||
album_id,
|
||||
track_id,
|
||||
} => {
|
||||
match self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into())
|
||||
.await
|
||||
{
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(history_vec))) => {
|
||||
let history_items_vec: Vec<LidarrHistoryItem> = history_vec
|
||||
.into_iter()
|
||||
.filter(|it| it.track_id == track_id)
|
||||
.collect();
|
||||
serde_json::to_string_pretty(&history_items_vec)?
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
|
||||
}
|
||||
}
|
||||
LidarrListCommand::Tracks {
|
||||
artist_id,
|
||||
album_id,
|
||||
} => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTracks(artist_id, album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::TrackFiles { album_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetTrackFiles(album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrListCommand::Updates => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetUpdates.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, list_command_handler::LidarrListCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_list_command_from() {
|
||||
let command = LidarrListCommand::Artists;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::List(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_list_commands_have_no_arg_requirements(
|
||||
#[values(
|
||||
"artists",
|
||||
"blocklist",
|
||||
"disk-space",
|
||||
"indexers",
|
||||
"metadata-profiles",
|
||||
"quality-profiles",
|
||||
"queued-events",
|
||||
"tags",
|
||||
"tasks",
|
||||
"updates",
|
||||
"root-folders"
|
||||
)]
|
||||
subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_albums_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "albums"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_albums_with_artist_id() {
|
||||
let expected_args = LidarrListCommand::Albums { artist_id: 1 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "lidarr", "list", "albums", "--artist-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(album_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(album_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_history_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"album-history",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_history_requires_album_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"album-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_album_history_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"album-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_artist_history_requires_artist_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artist-history"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_artist_history_success() {
|
||||
let expected_args = LidarrListCommand::ArtistHistory { artist_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"artist-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(artist_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(artist_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_downloads_count_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "downloads", "--count"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_downloads_default_values() {
|
||||
let expected_args = LidarrListCommand::Downloads { count: 500 };
|
||||
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "downloads"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(downloads_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(downloads_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "history", "--events"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_default_values() {
|
||||
let expected_args = LidarrListCommand::History { events: 500 };
|
||||
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "history"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(history_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(history_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "logs", "--events"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_default_values() {
|
||||
let expected_args = LidarrListCommand::Logs {
|
||||
events: 500,
|
||||
output_in_log_format: false,
|
||||
};
|
||||
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "logs"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(logs_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(logs_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_history_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-history",
|
||||
"--album-id",
|
||||
"1",
|
||||
"--track-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_history_requires_album_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--track-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_history_requires_track_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_history_success() {
|
||||
let expected_args = LidarrListCommand::TrackHistory {
|
||||
artist_id: 1,
|
||||
album_id: 1,
|
||||
track_id: 1,
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-history",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--album-id",
|
||||
"1",
|
||||
"--track-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(track_history_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(track_history_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tracks_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"tracks",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tracks_requires_album_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"tracks",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tracks_success() {
|
||||
let expected_args = LidarrListCommand::Tracks {
|
||||
artist_id: 1,
|
||||
album_id: 1,
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"tracks",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(tracks_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(tracks_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_files_requires_album_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "track-files"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_track_files_success() {
|
||||
let expected_args = LidarrListCommand::TrackFiles { album_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"list",
|
||||
"track-files",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Lidarr(LidarrCommand::List(track_files_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(track_files_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::lidarr::list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::lidarr_history_item;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)]
|
||||
#[case(LidarrListCommand::Blocklist, LidarrEvent::GetBlocklist)]
|
||||
#[case(LidarrListCommand::DiskSpace, LidarrEvent::GetDiskSpace)]
|
||||
#[case(LidarrListCommand::Indexers, LidarrEvent::GetIndexers)]
|
||||
#[case(LidarrListCommand::MetadataProfiles, LidarrEvent::GetMetadataProfiles)]
|
||||
#[case(LidarrListCommand::QualityProfiles, LidarrEvent::GetQualityProfiles)]
|
||||
#[case(LidarrListCommand::QueuedEvents, LidarrEvent::GetQueuedEvents)]
|
||||
#[case(LidarrListCommand::RootFolders, LidarrEvent::GetRootFolders)]
|
||||
#[case(LidarrListCommand::Tags, LidarrEvent::GetTags)]
|
||||
#[case(LidarrListCommand::Tasks, LidarrEvent::GetTasks)]
|
||||
#[case(LidarrListCommand::Updates, LidarrEvent::GetUpdates)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_command(
|
||||
#[case] list_command: LidarrListCommand,
|
||||
#[case] expected_lidarr_event: LidarrEvent,
|
||||
) {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(expected_lidarr_event.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
|
||||
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_albums_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::GetAlbums(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_command = LidarrListCommand::Albums { artist_id: 1 };
|
||||
|
||||
let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_album_history_command() {
|
||||
let expected_artist_id = 1;
|
||||
let expected_album_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetAlbumHistory(expected_artist_id, expected_album_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_album_history_command = LidarrListCommand::AlbumHistory {
|
||||
artist_id: 1,
|
||||
album_id: 1,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrListCommandHandler::with(&app_arc, list_album_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_artist_history_command() {
|
||||
let expected_artist_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetArtistHistory(expected_artist_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_artist_history_command = LidarrListCommand::ArtistHistory { artist_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrListCommandHandler::with(&app_arc, list_artist_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_downloads_command() {
|
||||
let expected_count = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetDownloads(expected_count).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_downloads_command = LidarrListCommand::Downloads { count: 1000 };
|
||||
|
||||
let result =
|
||||
LidarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_history_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetHistory(expected_events).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_history_command = LidarrListCommand::History { events: 1000 };
|
||||
|
||||
let result =
|
||||
LidarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_logs_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetLogs(expected_events).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_logs_command = LidarrListCommand::Logs {
|
||||
events: 1000,
|
||||
output_in_log_format: false,
|
||||
};
|
||||
|
||||
let result = LidarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_track_history_command() {
|
||||
let expected_artist_id = 1;
|
||||
let expected_album_id = 1;
|
||||
let expected_track_id = 5;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetTrackHistory(expected_artist_id, expected_album_id, expected_track_id)
|
||||
.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(
|
||||
vec![
|
||||
lidarr_history_item(),
|
||||
LidarrHistoryItem {
|
||||
track_id: 2,
|
||||
..lidarr_history_item()
|
||||
},
|
||||
],
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_track_history_command = LidarrListCommand::TrackHistory {
|
||||
artist_id: expected_artist_id,
|
||||
album_id: expected_album_id,
|
||||
track_id: expected_track_id,
|
||||
};
|
||||
|
||||
let result =
|
||||
LidarrListCommandHandler::with(&app_arc, list_track_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&[lidarr_history_item()]).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_tracks_command() {
|
||||
let expected_artist_id = 1;
|
||||
let expected_album_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetTracks(expected_artist_id, expected_album_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_tracks_command = LidarrListCommand::Tracks {
|
||||
artist_id: 1,
|
||||
album_id: 1,
|
||||
};
|
||||
|
||||
let result = LidarrListCommandHandler::with(&app_arc, list_tracks_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_track_files_command() {
|
||||
let expected_album_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetTrackFiles(expected_album_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_track_files_command = LidarrListCommand::TrackFiles { album_id: 1 };
|
||||
|
||||
let result =
|
||||
LidarrListCommandHandler::with(&app_arc, list_track_files_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use crate::app::App;
|
||||
use crate::cli::lidarr::LidarrCommand;
|
||||
use crate::cli::{CliCommandHandler, Command};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
|
||||
use crate::network::NetworkTrait;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "manual_search_command_handler_tests.rs"]
|
||||
mod manual_search_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrManualSearchCommand {
|
||||
#[command(
|
||||
about = "Trigger a manual search of releases for the given album corresponding to the artist with the given ID"
|
||||
)]
|
||||
Album {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist whose releases you wish to fetch and list",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
#[arg(long, help = "The Lidarr album ID to search for", required = true)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Trigger a manual search of discography releases for the given artist corresponding to the artist with the given ID."
|
||||
)]
|
||||
Discography {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist whose discography releases you wish to fetch and list",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrManualSearchCommand> for Command {
|
||||
fn from(value: LidarrManualSearchCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::ManualSearch(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrManualSearchCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrManualSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrManualSearchCommand>
|
||||
for LidarrManualSearchCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrManualSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrManualSearchCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrManualSearchCommand::Album {
|
||||
artist_id,
|
||||
album_id,
|
||||
} => {
|
||||
println!("Searching for album releases. This may take a minute...");
|
||||
match self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetAlbumReleases(artist_id, album_id).into())
|
||||
.await
|
||||
{
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => {
|
||||
let albums_vec: Vec<LidarrRelease> = releases_vec
|
||||
.into_iter()
|
||||
.filter(|release| !release.discography)
|
||||
.collect();
|
||||
serde_json::to_string_pretty(&albums_vec)?
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
|
||||
}
|
||||
}
|
||||
LidarrManualSearchCommand::Discography { artist_id } => {
|
||||
println!("Searching for artist discography releases. This may take a minute...");
|
||||
match self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetDiscographyReleases(artist_id).into())
|
||||
.await
|
||||
{
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => {
|
||||
let discography_vec: Vec<LidarrRelease> = releases_vec
|
||||
.into_iter()
|
||||
.filter(|release| release.discography)
|
||||
.collect();
|
||||
serde_json::to_string_pretty(&discography_vec)?
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::Command;
|
||||
use crate::cli::lidarr::LidarrCommand;
|
||||
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_manual_search_command_from() {
|
||||
let command = LidarrManualSearchCommand::Discography { artist_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Command::Lidarr(LidarrCommand::ManualSearch(command))
|
||||
);
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use crate::Cli;
|
||||
use clap::CommandFactory;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_manual_album_search_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"manual-search",
|
||||
"album",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_album_search_requires_album_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"manual-search",
|
||||
"album",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_album_search_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"manual-search",
|
||||
"album",
|
||||
"--artist-id",
|
||||
"1",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_discography_search_requires_artist_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "manual-search", "discography"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_discography_search_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"manual-search",
|
||||
"discography",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use crate::app::App;
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::lidarr::manual_search_command_handler::{
|
||||
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
|
||||
};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
torrent_release, usenet_release,
|
||||
};
|
||||
use crate::network::{MockNetworkTrait, NetworkEvent};
|
||||
use mockall::predicate::eq;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manual_album_search_command() {
|
||||
let expected_releases = [torrent_release()];
|
||||
let expected_artist_id = 1;
|
||||
let expected_album_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetAlbumReleases(expected_artist_id, expected_album_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
|
||||
torrent_release(),
|
||||
LidarrRelease {
|
||||
discography: true,
|
||||
..usenet_release()
|
||||
},
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_album_search_command = LidarrManualSearchCommand::Album {
|
||||
artist_id: 1,
|
||||
album_id: 1,
|
||||
};
|
||||
|
||||
let result = LidarrManualSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
manual_album_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&expected_releases).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manual_discography_search_command() {
|
||||
let expected_releases = [LidarrRelease {
|
||||
discography: true,
|
||||
..usenet_release()
|
||||
}];
|
||||
let expected_artist_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::GetDiscographyReleases(expected_artist_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![
|
||||
torrent_release(),
|
||||
LidarrRelease {
|
||||
discography: true,
|
||||
..usenet_release()
|
||||
},
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_discography_search_command =
|
||||
LidarrManualSearchCommand::Discography { artist_id: 1 };
|
||||
|
||||
let result = LidarrManualSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
manual_discography_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&expected_releases).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
|
||||
use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler};
|
||||
use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
|
||||
use list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||
use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use trigger_automatic_search_command_handler::{
|
||||
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
|
||||
use super::{CliCommandHandler, Command};
|
||||
use crate::cli::lidarr::manual_search_command_handler::{
|
||||
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
|
||||
};
|
||||
use crate::models::lidarr_models::LidarrTaskName;
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
mod add_command_handler;
|
||||
mod delete_command_handler;
|
||||
mod edit_command_handler;
|
||||
mod get_command_handler;
|
||||
mod list_command_handler;
|
||||
mod manual_search_command_handler;
|
||||
mod refresh_command_handler;
|
||||
mod trigger_automatic_search_command_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_command_tests.rs"]
|
||||
mod lidarr_command_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrCommand {
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to add or create new resources within your Lidarr instance"
|
||||
)]
|
||||
Add(LidarrAddCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to delete resources from your Lidarr instance"
|
||||
)]
|
||||
Delete(LidarrDeleteCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to edit resources in your Lidarr instance"
|
||||
)]
|
||||
Edit(LidarrEditCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to fetch details of the resources in your Lidarr instance"
|
||||
)]
|
||||
Get(LidarrGetCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to list attributes from your Lidarr instance"
|
||||
)]
|
||||
List(LidarrListCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to refresh the data in your Lidarr instance"
|
||||
)]
|
||||
Refresh(LidarrRefreshCommand),
|
||||
#[command(subcommand, about = "Commands to manually search for releases")]
|
||||
ManualSearch(LidarrManualSearchCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance"
|
||||
)]
|
||||
TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand),
|
||||
#[command(about = "Clear the Lidarr blocklist")]
|
||||
ClearBlocklist,
|
||||
#[command(about = "Manually download the given release")]
|
||||
DownloadRelease {
|
||||
#[arg(long, help = "The GUID of the release to download", required = true)]
|
||||
guid: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The indexer ID to download the release from",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Mark the Lidarr history item with the given ID as 'failed'")]
|
||||
MarkHistoryItemAsFailed {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the history item you wish to mark as 'failed'",
|
||||
required = true
|
||||
)]
|
||||
history_item_id: i64,
|
||||
},
|
||||
#[command(about = "Search for a new artist to add to Lidarr")]
|
||||
SearchNewArtist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The name of the artist you want to search for",
|
||||
required = true
|
||||
)]
|
||||
query: String,
|
||||
},
|
||||
#[command(about = "Start the specified Lidarr task")]
|
||||
StartTask {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The name of the task to trigger",
|
||||
value_enum,
|
||||
required = true
|
||||
)]
|
||||
task_name: LidarrTaskName,
|
||||
},
|
||||
#[command(
|
||||
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
|
||||
)]
|
||||
TestIndexer {
|
||||
#[arg(long, help = "The ID of the indexer to test", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Test all Lidarr indexers")]
|
||||
TestAllIndexers,
|
||||
#[command(
|
||||
about = "Toggle monitoring for the specified album corresponding to the given album ID"
|
||||
)]
|
||||
ToggleAlbumMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the album to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Toggle monitoring for the specified artist corresponding to the given artist ID"
|
||||
)]
|
||||
ToggleArtistMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the artist to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrCommand> for Command {
|
||||
fn from(lidarr_command: LidarrCommand) -> Command {
|
||||
Command::Lidarr(lidarr_command)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrCliHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrCliHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrCommand::Add(add_command) => {
|
||||
LidarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::Delete(delete_command) => {
|
||||
LidarrDeleteCommandHandler::with(self.app, delete_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::Edit(edit_command) => {
|
||||
LidarrEditCommandHandler::with(self.app, edit_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::Get(get_command) => {
|
||||
LidarrGetCommandHandler::with(self.app, get_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::List(list_command) => {
|
||||
LidarrListCommandHandler::with(self.app, list_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::Refresh(refresh_command) => {
|
||||
LidarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::ManualSearch(manual_search_command) => {
|
||||
LidarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
|
||||
LidarrTriggerAutomaticSearchCommandHandler::with(
|
||||
self.app,
|
||||
trigger_automatic_search_command,
|
||||
self.network,
|
||||
)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
LidarrCommand::ClearBlocklist => {
|
||||
self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ClearBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::DownloadRelease { guid, indexer_id } => {
|
||||
let params = ReleaseDownloadBody { guid, indexer_id };
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DownloadRelease(params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
|
||||
let _ = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&json!({"message": "Lidarr history item marked as 'failed'"}))?
|
||||
}
|
||||
LidarrCommand::SearchNewArtist { query } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::SearchNewArtist(query).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::StartTask { task_name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::StartTask(task_name).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::TestIndexer { indexer_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::TestIndexer(indexer_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::TestAllIndexers => {
|
||||
println!("Testing all Lidarr indexers. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::TestAllIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::ToggleAlbumMonitoring { album_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ToggleAlbumMonitoring(album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::ToggleArtistMonitoring { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::ToggleArtistMonitoring(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "refresh_command_handler_tests.rs"]
|
||||
mod refresh_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrRefreshCommand {
|
||||
#[command(about = "Refresh all artist data for all artists in your Lidarr library")]
|
||||
AllArtists,
|
||||
#[command(about = "Refresh artist data and scan disk for the artist with the given ID")]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the artist to refresh information on and to scan the disk for",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
#[command(about = "Refresh all downloads in Lidarr")]
|
||||
Downloads,
|
||||
}
|
||||
|
||||
impl From<LidarrRefreshCommand> for Command {
|
||||
fn from(value: LidarrRefreshCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::Refresh(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrRefreshCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
|
||||
for LidarrRefreshCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrRefreshCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> anyhow::Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrRefreshCommand::AllArtists => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::UpdateAllArtists.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrRefreshCommand::Artist { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::UpdateAndScanArtist(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrRefreshCommand::Downloads => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::UpdateDownloads.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, refresh_command_handler::LidarrRefreshCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_refresh_command_from() {
|
||||
let command = LidarrRefreshCommand::AllArtists;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::Refresh(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_refresh_commands_have_no_arg_requirements(
|
||||
#[values("all-artists", "downloads")] subcommand: &str,
|
||||
) {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_artist_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "artist"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_artist_with_artist_id() {
|
||||
let expected_args = LidarrRefreshCommand::Artist { artist_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"refresh",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Lidarr(LidarrCommand::Refresh(refresh_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{app::App, cli::lidarr::refresh_command_handler::LidarrRefreshCommandHandler};
|
||||
use crate::{
|
||||
cli::{CliCommandHandler, lidarr::refresh_command_handler::LidarrRefreshCommand},
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
use crate::{
|
||||
models::{Serdeable, lidarr_models::LidarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[case(LidarrRefreshCommand::AllArtists, LidarrEvent::UpdateAllArtists)]
|
||||
#[case(LidarrRefreshCommand::Downloads, LidarrEvent::UpdateDownloads)]
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_command(
|
||||
#[case] refresh_command: LidarrRefreshCommand,
|
||||
#[case] expected_sonarr_event: LidarrEvent,
|
||||
) {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
|
||||
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_artist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::UpdateAndScanArtist(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = LidarrRefreshCommand::Artist { artist_id: 1 };
|
||||
|
||||
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "trigger_automatic_search_command_handler_tests.rs"]
|
||||
mod trigger_automatic_search_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrTriggerAutomaticSearchCommand {
|
||||
#[command(about = "Trigger an automatic search for the album with the specified ID")]
|
||||
Album {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Lidarr ID of the album you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
album_id: i64,
|
||||
},
|
||||
#[command(about = "Trigger an automatic search for the artist with the specified ID")]
|
||||
Artist {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the artist you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
artist_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LidarrTriggerAutomaticSearchCommand> for Command {
|
||||
fn from(value: LidarrTriggerAutomaticSearchCommand) -> Self {
|
||||
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct LidarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrTriggerAutomaticSearchCommand>
|
||||
for LidarrTriggerAutomaticSearchCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: LidarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
LidarrTriggerAutomaticSearchCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
LidarrTriggerAutomaticSearchCommand::Album { album_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::TriggerAutomaticAlbumSearch(album_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrTriggerAutomaticSearchCommand::Artist { artist_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::TriggerAutomaticArtistSearch(artist_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{
|
||||
LidarrCommand, trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand,
|
||||
},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_trigger_automatic_search_command_from() {
|
||||
let command = LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(command))
|
||||
);
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_album_search_requires_album_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"trigger-automatic-search",
|
||||
"album",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_album_search_with_album_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"trigger-automatic-search",
|
||||
"album",
|
||||
"--album-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_artist_search_requires_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"trigger-automatic-search",
|
||||
"artist",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_artist_search_with_artist_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"lidarr",
|
||||
"trigger-automatic-search",
|
||||
"artist",
|
||||
"--artist-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::lidarr::trigger_automatic_search_command_handler::{
|
||||
LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
use crate::{app::App, cli::CliCommandHandler};
|
||||
use crate::{
|
||||
models::{Serdeable, lidarr_models::LidarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_album_search_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::TriggerAutomaticAlbumSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
LidarrTriggerAutomaticSearchCommand::Album { album_id: 1 };
|
||||
|
||||
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_artist_search_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
LidarrEvent::TriggerAutomaticArtistSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 };
|
||||
|
||||
let result = LidarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use clap_complete::Shell;
|
||||
use indoc::indoc;
|
||||
use lidarr::{LidarrCliHandler, LidarrCommand};
|
||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||
use readarr::{ReadarrCliHandler, ReadarrCommand};
|
||||
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod readarr;
|
||||
pub mod sonarr;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -24,6 +29,12 @@ pub enum Command {
|
||||
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
|
||||
Sonarr(SonarrCommand),
|
||||
|
||||
#[command(subcommand, about = "Commands for manging your Lidarr instance")]
|
||||
Lidarr(LidarrCommand),
|
||||
|
||||
#[command(subcommand, about = "Commands for managing your Readarr instance")]
|
||||
Readarr(ReadarrCommand),
|
||||
|
||||
#[command(
|
||||
arg_required_else_help = true,
|
||||
about = "Generate shell completions for the Managarr CLI"
|
||||
@@ -38,6 +49,12 @@ pub enum Command {
|
||||
#[arg(long, help = "Disable colored log output")]
|
||||
no_color: bool,
|
||||
},
|
||||
|
||||
#[command(about = indoc!{"
|
||||
Print the full path to the default configuration file.
|
||||
This file can be changed to another location using the '--config-file' flag
|
||||
"})]
|
||||
ConfigPath,
|
||||
}
|
||||
|
||||
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
|
||||
@@ -61,6 +78,16 @@ pub(crate) async fn handle_command(
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Command::Lidarr(lidarr_command) => {
|
||||
LidarrCliHandler::with(app, lidarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Command::Readarr(readarr_command) => {
|
||||
ReadarrCliHandler::with(app, readarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand, arg, command};
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::RadarrCommand;
|
||||
@@ -122,12 +122,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
|
||||
title: String::new(),
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
minimum_availability: minimum_availability.to_string(),
|
||||
minimum_availability,
|
||||
monitored: !disable_monitoring,
|
||||
tags,
|
||||
tag_input_string: None,
|
||||
add_options: AddMovieOptions {
|
||||
monitor: monitor.to_string(),
|
||||
monitor,
|
||||
search_for_movie: !no_search_for_movie,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -384,12 +384,12 @@ mod tests {
|
||||
title: String::new(),
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
minimum_availability: "released".to_owned(),
|
||||
minimum_availability: MinimumAvailability::Released,
|
||||
monitored: false,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
add_options: AddMovieOptions {
|
||||
monitor: "movieAndCollection".to_owned(),
|
||||
monitor: MovieMonitor::MovieAndCollection,
|
||||
search_for_movie: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -833,13 +833,13 @@ mod tests {
|
||||
async fn test_handle_edit_all_indexer_settings_command() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
availability_delay: 1,
|
||||
availability_delay: 19,
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
maximum_size: 18400,
|
||||
minimum_age: 27,
|
||||
prefer_indexer_flags: true,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
retention: 44,
|
||||
rss_sync_interval: 60,
|
||||
whitelisted_hardcoded_subs: "test".into(),
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
@@ -853,13 +853,13 @@ mod tests {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
allow_hardcoded_subs: false,
|
||||
availability_delay: 2,
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
availability_delay: 31,
|
||||
id: 8,
|
||||
maximum_size: 22750,
|
||||
minimum_age: 52,
|
||||
prefer_indexer_flags: false,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
retention: 66,
|
||||
rss_sync_interval: 90,
|
||||
whitelisted_hardcoded_subs: "testing".into(),
|
||||
},
|
||||
)))
|
||||
@@ -879,13 +879,13 @@ mod tests {
|
||||
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
disable_allow_hardcoded_subs: false,
|
||||
availability_delay: Some(1),
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
availability_delay: Some(19),
|
||||
maximum_size: Some(18400),
|
||||
minimum_age: Some(27),
|
||||
prefer_indexer_flags: true,
|
||||
disable_prefer_indexer_flags: false,
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
retention: Some(44),
|
||||
rss_sync_interval: Some(60),
|
||||
whitelisted_subtitle_tags: Some("test".to_owned()),
|
||||
};
|
||||
|
||||
@@ -904,13 +904,13 @@ mod tests {
|
||||
async fn test_handle_edit_all_indexer_settings_command_disable_flags_function_properly() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
allow_hardcoded_subs: false,
|
||||
availability_delay: 1,
|
||||
availability_delay: 19,
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
maximum_size: 18400,
|
||||
minimum_age: 27,
|
||||
prefer_indexer_flags: false,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
retention: 44,
|
||||
rss_sync_interval: 60,
|
||||
whitelisted_hardcoded_subs: "test".into(),
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
@@ -924,13 +924,13 @@ mod tests {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
availability_delay: 2,
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
availability_delay: 31,
|
||||
id: 8,
|
||||
maximum_size: 22750,
|
||||
minimum_age: 52,
|
||||
prefer_indexer_flags: true,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
retention: 66,
|
||||
rss_sync_interval: 90,
|
||||
whitelisted_hardcoded_subs: "testing".into(),
|
||||
},
|
||||
)))
|
||||
@@ -950,13 +950,13 @@ mod tests {
|
||||
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
|
||||
allow_hardcoded_subs: false,
|
||||
disable_allow_hardcoded_subs: true,
|
||||
availability_delay: Some(1),
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
availability_delay: Some(19),
|
||||
maximum_size: Some(18400),
|
||||
minimum_age: Some(27),
|
||||
prefer_indexer_flags: false,
|
||||
disable_prefer_indexer_flags: true,
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
retention: Some(44),
|
||||
rss_sync_interval: Some(60),
|
||||
whitelisted_subtitle_tags: Some("test".to_owned()),
|
||||
};
|
||||
|
||||
@@ -976,13 +976,13 @@ mod tests {
|
||||
{
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
availability_delay: 2,
|
||||
availability_delay: 31,
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
prefer_indexer_flags: true,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
maximum_size: 22750,
|
||||
minimum_age: 52,
|
||||
prefer_indexer_flags: false,
|
||||
retention: 66,
|
||||
rss_sync_interval: 90,
|
||||
whitelisted_hardcoded_subs: "testing".into(),
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
@@ -996,13 +996,13 @@ mod tests {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
availability_delay: 2,
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
prefer_indexer_flags: true,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
availability_delay: 31,
|
||||
id: 8,
|
||||
maximum_size: 22750,
|
||||
minimum_age: 52,
|
||||
prefer_indexer_flags: false,
|
||||
retention: 66,
|
||||
rss_sync_interval: 90,
|
||||
whitelisted_hardcoded_subs: "testing".into(),
|
||||
},
|
||||
)))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
@@ -29,6 +29,11 @@ pub enum RadarrListCommand {
|
||||
},
|
||||
#[command(about = "List disk space details for all provisioned root folders in Radarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "Fetch all Radarr history events")]
|
||||
History {
|
||||
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
},
|
||||
#[command(about = "List all Radarr indexers")]
|
||||
Indexers,
|
||||
#[command(about = "Fetch Radarr logs")]
|
||||
@@ -121,6 +126,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::History { events: items } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::GetHistory(items).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrListCommand::Indexers => {
|
||||
let resp = self
|
||||
.network
|
||||
|
||||
@@ -111,6 +111,29 @@ mod tests {
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "history", "--events"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_default_values() {
|
||||
let expected_args = RadarrListCommand::History { events: 500 };
|
||||
let result = Cli::try_parse_from(["managarr", "radarr", "list", "history"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Radarr(RadarrCommand::List(history_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(history_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_default_values() {
|
||||
let expected_args = RadarrListCommand::Logs {
|
||||
@@ -233,6 +256,32 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_history_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
RadarrEvent::GetHistory(expected_events).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_history_command = RadarrListCommand::History { events: 1000 };
|
||||
|
||||
let result =
|
||||
RadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_logs_command() {
|
||||
let expected_events = 1000;
|
||||
|
||||
@@ -64,6 +64,15 @@ pub enum RadarrCommand {
|
||||
Refresh(RadarrRefreshCommand),
|
||||
#[command(about = "Clear the blocklist")]
|
||||
ClearBlocklist,
|
||||
#[command(about = "Mark the Radarr history item with the given ID as 'failed'")]
|
||||
MarkHistoryItemAsFailed {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Radarr ID of the history item you wish to mark as 'failed'",
|
||||
required = true
|
||||
)]
|
||||
history_item_id: i64,
|
||||
},
|
||||
#[command(about = "Manually download the given release for the specified movie ID")]
|
||||
DownloadRelease {
|
||||
#[arg(long, help = "The GUID of the release to download", required = true)]
|
||||
@@ -208,6 +217,15 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
RadarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
|
||||
let _ = self
|
||||
.network
|
||||
.handle_network_event(RadarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(
|
||||
&serde_json::json!({"message": "Radarr history item marked as 'failed'"}),
|
||||
)?
|
||||
}
|
||||
RadarrCommand::DownloadRelease {
|
||||
guid,
|
||||
indexer_id,
|
||||
|
||||
@@ -31,6 +31,31 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requires_history_item_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "radarr", "mark-history-item-as-failed"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"radarr",
|
||||
"mark-history-item-as-failed",
|
||||
"--history-item-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requires_movie_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
@@ -327,6 +352,36 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_history_item_as_failed_command() {
|
||||
let expected_history_item_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
RadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let mark_history_item_as_failed_command =
|
||||
RadarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 };
|
||||
|
||||
let result = RadarrCliHandler::with(
|
||||
&app_arc,
|
||||
mark_history_item_as_failed_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_release_command() {
|
||||
let expected_release_download_body = RadarrReleaseDownloadBody {
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::ReadarrCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::readarr_models::{
|
||||
AddAuthorBody, AddAuthorOptions, AddReadarrRootFolderBody, MonitorType, NewItemMonitorType,
|
||||
},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "add_command_handler_tests.rs"]
|
||||
mod add_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrAddCommand {
|
||||
#[command(about = "Add a new author to your Readarr library")]
|
||||
Author {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Goodreads foreign author ID of the author you wish to add to your library",
|
||||
required = true
|
||||
)]
|
||||
foreign_author_id: String,
|
||||
#[arg(long, help = "The name of the author", required = true)]
|
||||
author_name: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all author data and metadata should live",
|
||||
required = true
|
||||
)]
|
||||
root_folder_path: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the quality profile to use for this author",
|
||||
required = true
|
||||
)]
|
||||
quality_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the metadata profile to use for this author",
|
||||
required = true
|
||||
)]
|
||||
metadata_profile_id: i64,
|
||||
#[arg(long, help = "Disable monitoring for this author")]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag the author with",
|
||||
value_parser,
|
||||
action = ArgAction::Append
|
||||
)]
|
||||
tag: Vec<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "What Readarr should monitor for this author",
|
||||
value_enum,
|
||||
default_value_t = MonitorType::default()
|
||||
)]
|
||||
monitor: MonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "How Readarr should monitor new items for this author",
|
||||
value_enum,
|
||||
default_value_t = NewItemMonitorType::default()
|
||||
)]
|
||||
monitor_new_items: NewItemMonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tell Readarr to not start a search for missing books once the author is added to your library"
|
||||
)]
|
||||
no_search_for_missing_books: bool,
|
||||
},
|
||||
#[command(about = "Add a new root folder")]
|
||||
RootFolder {
|
||||
#[arg(long, help = "The name of the root folder", required = true)]
|
||||
name: String,
|
||||
#[arg(long, help = "The path of the new root folder", required = true)]
|
||||
root_folder_path: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the default quality profile for authors in this root folder",
|
||||
required = true
|
||||
)]
|
||||
quality_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the default metadata profile for authors in this root folder",
|
||||
required = true
|
||||
)]
|
||||
metadata_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The default monitor option for authors in this root folder",
|
||||
value_enum,
|
||||
default_value_t = MonitorType::default()
|
||||
)]
|
||||
monitor: MonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The default monitor new items option for authors in this root folder",
|
||||
value_enum,
|
||||
default_value_t = NewItemMonitorType::default()
|
||||
)]
|
||||
monitor_new_items: NewItemMonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Default tag IDs for authors in this root folder",
|
||||
value_parser,
|
||||
action = ArgAction::Append
|
||||
)]
|
||||
tag: Vec<i64>,
|
||||
},
|
||||
#[command(about = "Add new tag")]
|
||||
Tag {
|
||||
#[arg(long, help = "The name of the tag to be added", required = true)]
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrAddCommand> for Command {
|
||||
fn from(value: ReadarrAddCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Add(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrAddCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrAddCommand> for ReadarrAddCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrAddCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrAddCommand::Author {
|
||||
foreign_author_id,
|
||||
author_name,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
disable_monitoring,
|
||||
tag: tags,
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
no_search_for_missing_books,
|
||||
} => {
|
||||
let body = AddAuthorBody {
|
||||
foreign_author_id,
|
||||
author_name,
|
||||
monitored: !disable_monitoring,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
tags,
|
||||
images: Vec::new(),
|
||||
tag_input_string: None,
|
||||
add_options: AddAuthorOptions {
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
search_for_missing_books: !no_search_for_missing_books,
|
||||
},
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::AddAuthor(body).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrAddCommand::RootFolder {
|
||||
name,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
tag: tags,
|
||||
} => {
|
||||
let add_root_folder_body = AddReadarrRootFolderBody {
|
||||
name,
|
||||
path: root_folder_path,
|
||||
default_quality_profile_id: quality_profile_id,
|
||||
default_metadata_profile_id: metadata_profile_id,
|
||||
default_monitor_option: monitor,
|
||||
default_new_item_monitor_option: monitor_new_items,
|
||||
default_tags: tags,
|
||||
tag_input_string: None,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::AddRootFolder(add_root_folder_body).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrAddCommand::Tag { name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::AddTag(name).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, add_command_handler::ReadarrAddCommand},
|
||||
},
|
||||
models::readarr_models::{MonitorType, NewItemMonitorType},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_add_command_from() {
|
||||
let command = ReadarrAddCommand::Tag {
|
||||
name: String::new(),
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::Add(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "add", "author"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_foreign_author_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--author-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_author_name() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_root_folder_path() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_quality_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_metadata_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_success_with_required_args_only() {
|
||||
let expected_args = ReadarrAddCommand::Author {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
disable_monitoring: false,
|
||||
tag: vec![],
|
||||
monitor: MonitorType::default(),
|
||||
monitor_new_items: NewItemMonitorType::default(),
|
||||
no_search_for_missing_books: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_success_with_all_args() {
|
||||
let expected_args = ReadarrAddCommand::Author {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 2,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::Future,
|
||||
monitor_new_items: NewItemMonitorType::New,
|
||||
no_search_for_missing_books: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--disable-monitoring",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
"--monitor",
|
||||
"future",
|
||||
"--monitor-new-items",
|
||||
"new",
|
||||
"--no-search-for-missing-books",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_monitor_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--monitor",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_new_item_monitor_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--monitor-new-items",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_tags_is_repeatable() {
|
||||
let expected_args = ReadarrAddCommand::Author {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 2,
|
||||
disable_monitoring: false,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::default(),
|
||||
monitor_new_items: NewItemMonitorType::default(),
|
||||
no_search_for_missing_books: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_root_folder_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "add", "root-folder"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_root_folder_success() {
|
||||
let expected_args = ReadarrAddCommand::RootFolder {
|
||||
name: "Books".to_owned(),
|
||||
root_folder_path: "/nfs/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
tag: vec![],
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"root-folder",
|
||||
"--name",
|
||||
"Books",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "add", "tag"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_success() {
|
||||
let expected_args = ReadarrAddCommand::Tag {
|
||||
name: "test".to_owned(),
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "add", "tag", "--name", "test"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::readarr::add_command_handler::{ReadarrAddCommand, ReadarrAddCommandHandler};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::readarr_models::{
|
||||
AddAuthorBody, AddAuthorOptions, AddReadarrRootFolderBody, MonitorType, NewItemMonitorType,
|
||||
ReadarrSerdeable,
|
||||
};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_author_command() {
|
||||
let expected_body = AddAuthorBody {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
monitored: false,
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
tags: vec![1, 2],
|
||||
images: Vec::new(),
|
||||
tag_input_string: None,
|
||||
add_options: AddAuthorOptions {
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
search_for_missing_books: false,
|
||||
},
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::AddAuthor(expected_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_author_command = ReadarrAddCommand::Author {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
no_search_for_missing_books: true,
|
||||
};
|
||||
|
||||
let result = ReadarrAddCommandHandler::with(&app_arc, add_author_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_root_folder_command() {
|
||||
let expected_root_folder_path = "/nfs/test".to_owned();
|
||||
let expected_add_root_folder_body = AddReadarrRootFolderBody {
|
||||
name: "Books".to_owned(),
|
||||
path: expected_root_folder_path.clone(),
|
||||
default_quality_profile_id: 1,
|
||||
default_metadata_profile_id: 1,
|
||||
default_monitor_option: MonitorType::All,
|
||||
default_new_item_monitor_option: NewItemMonitorType::All,
|
||||
default_tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_root_folder_command = ReadarrAddCommand::RootFolder {
|
||||
name: "Books".to_owned(),
|
||||
root_folder_path: expected_root_folder_path,
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
tag: vec![1, 2],
|
||||
};
|
||||
|
||||
let result =
|
||||
ReadarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_tag_command() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_tag_command = ReadarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
};
|
||||
|
||||
let result = ReadarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::ReadarrCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::readarr_models::DeleteParams,
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_command_handler_tests.rs"]
|
||||
mod delete_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrDeleteCommand {
|
||||
#[command(about = "Delete an author from your Readarr library")]
|
||||
Author {
|
||||
#[arg(long, help = "The ID of the author to delete", required = true)]
|
||||
author_id: i64,
|
||||
#[arg(long, help = "Delete the author files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this author")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the specified item from the Readarr blocklist")]
|
||||
BlocklistItem {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the blocklist item to remove from the blocklist",
|
||||
required = true
|
||||
)]
|
||||
blocklist_item_id: i64,
|
||||
},
|
||||
#[command(about = "Delete a book from your Readarr library")]
|
||||
Book {
|
||||
#[arg(long, help = "The ID of the book to delete", required = true)]
|
||||
book_id: i64,
|
||||
#[arg(long, help = "Delete the book files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this book")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the specified book file from disk")]
|
||||
BookFile {
|
||||
#[arg(long, help = "The ID of the book file to delete", required = true)]
|
||||
book_file_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the specified download")]
|
||||
Download {
|
||||
#[arg(long, help = "The ID of the download to delete", required = true)]
|
||||
download_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the indexer with the given ID")]
|
||||
Indexer {
|
||||
#[arg(long, help = "The ID of the indexer to delete", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the root folder with the given ID")]
|
||||
RootFolder {
|
||||
#[arg(long, help = "The ID of the root folder to delete", required = true)]
|
||||
root_folder_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the tag with the specified ID")]
|
||||
Tag {
|
||||
#[arg(long, help = "The ID of the tag to delete", required = true)]
|
||||
tag_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrDeleteCommand> for Command {
|
||||
fn from(value: ReadarrDeleteCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Delete(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrDeleteCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrDeleteCommand>
|
||||
for ReadarrDeleteCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrDeleteCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrDeleteCommand::Author {
|
||||
author_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_author_params = DeleteParams {
|
||||
id: author_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteAuthor(delete_author_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteBlocklistItem(blocklist_item_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::Book {
|
||||
book_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_book_params = DeleteParams {
|
||||
id: book_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteBook(delete_book_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::BookFile { book_file_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteBookFile(book_file_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::Download { download_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteDownload(download_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::Indexer { indexer_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteIndexer(indexer_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::RootFolder { root_folder_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteRootFolder(root_folder_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::Tag { tag_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteTag(tag_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, delete_command_handler::ReadarrDeleteCommand},
|
||||
},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_delete_command_from() {
|
||||
let command = ReadarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::Delete(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_delete_author_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "author"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_author_author_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"author",
|
||||
"--author-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_author_defaults() {
|
||||
let expected_args = ReadarrDeleteCommand::Author {
|
||||
author_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"author",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_author_all_args_defined() {
|
||||
let expected_args = ReadarrDeleteCommand::Author {
|
||||
author_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"author",
|
||||
"--author-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "blocklist-item"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_blocklist_item_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"blocklist-item",
|
||||
"--blocklist-item-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_success() {
|
||||
let expected_args = ReadarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 7,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"blocklist-item",
|
||||
"--blocklist-item-id",
|
||||
"7",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "book"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_book_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"book",
|
||||
"--book-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_defaults() {
|
||||
let expected_args = ReadarrDeleteCommand::Book {
|
||||
book_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "delete", "book", "--book-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_all_args_defined() {
|
||||
let expected_args = ReadarrDeleteCommand::Book {
|
||||
book_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"book",
|
||||
"--book-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_file_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "book-file"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_file_book_file_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"book-file",
|
||||
"--book-file-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_file_success() {
|
||||
let expected_args = ReadarrDeleteCommand::BookFile { book_file_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"book-file",
|
||||
"--book-file-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "download"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_download_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"download",
|
||||
"--download-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_success() {
|
||||
let expected_args = ReadarrDeleteCommand::Download { download_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"download",
|
||||
"--download-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "indexer"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_indexer_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_success() {
|
||||
let expected_args = ReadarrDeleteCommand::Indexer { indexer_id: 8 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"8",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "root-folder"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_root_folder_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"root-folder",
|
||||
"--root-folder-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_success() {
|
||||
let expected_args = ReadarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"root-folder",
|
||||
"--root-folder-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "tag"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_tag_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"tag",
|
||||
"--tag-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_success() {
|
||||
let expected_args = ReadarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "delete", "tag", "--tag-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::readarr::delete_command_handler::{
|
||||
ReadarrDeleteCommand, ReadarrDeleteCommandHandler,
|
||||
};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::readarr_models::{DeleteParams, ReadarrSerdeable};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_author_command() {
|
||||
let expected_delete_author_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DeleteAuthor(expected_delete_author_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_author_command = ReadarrDeleteCommand::Author {
|
||||
author_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_author_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_book_command() {
|
||||
let expected_delete_book_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DeleteBook(expected_delete_book_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_book_command = ReadarrDeleteCommand::Book {
|
||||
book_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_book_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_blocklist_item_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DeleteBlocklistItem(7).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_blocklist_item_command = ReadarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 7,
|
||||
};
|
||||
|
||||
let result = ReadarrDeleteCommandHandler::with(
|
||||
&app_arc,
|
||||
delete_blocklist_item_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_book_file_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteBookFile(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_book_file_command = ReadarrDeleteCommand::BookFile { book_file_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_book_file_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_download_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteDownload(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_download_command = ReadarrDeleteCommand::Download { download_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_indexer_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteIndexer(8).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_indexer_command = ReadarrDeleteCommand::Indexer { indexer_id: 8 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_root_folder_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteRootFolder(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_root_folder_command = ReadarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_tag_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteTag(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_tag_command = ReadarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, ArgGroup, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::ReadarrCommand;
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::readarr_models::ReadarrSerdeable;
|
||||
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command, mutex_flags_or_option},
|
||||
models::readarr_models::{EditAuthorParams, NewItemMonitorType},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_command_handler_tests.rs"]
|
||||
mod edit_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrEditCommand {
|
||||
#[command(
|
||||
about = "Edit the indexer settings that apply to all indexers",
|
||||
group(
|
||||
ArgGroup::new("edit_settings")
|
||||
.args([
|
||||
"maximum_size",
|
||||
"minimum_age",
|
||||
"retention",
|
||||
"rss_sync_interval",
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
AllIndexerSettings {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
|
||||
)]
|
||||
maximum_size: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
|
||||
)]
|
||||
minimum_age: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
|
||||
)]
|
||||
retention: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
|
||||
)]
|
||||
rss_sync_interval: Option<i64>,
|
||||
},
|
||||
#[command(
|
||||
about = "Edit preferences for the specified author",
|
||||
group(
|
||||
ArgGroup::new("edit_author")
|
||||
.args([
|
||||
"enable_monitoring",
|
||||
"disable_monitoring",
|
||||
"monitor_new_items",
|
||||
"quality_profile_id",
|
||||
"metadata_profile_id",
|
||||
"root_folder_path",
|
||||
"tag",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Author {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the author whose settings you want to edit",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Enable monitoring of this author in Readarr so Readarr will automatically download books from this author if they are available",
|
||||
conflicts_with = "disable_monitoring"
|
||||
)]
|
||||
enable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable monitoring of this author so Readarr does not automatically download books from this author if they are available",
|
||||
conflicts_with = "enable_monitoring"
|
||||
)]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "How Readarr should monitor new books from this author",
|
||||
value_enum
|
||||
)]
|
||||
monitor_new_items: Option<NewItemMonitorType>,
|
||||
#[arg(long, help = "The ID of the quality profile to use for this author")]
|
||||
quality_profile_id: Option<i64>,
|
||||
#[arg(long, help = "The ID of the metadata profile to use for this author")]
|
||||
metadata_profile_id: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all author data and metadata should live"
|
||||
)]
|
||||
root_folder_path: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag this author with",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(long, help = "Clear all tags on this author", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
#[command(
|
||||
about = "Edit preferences for the specified indexer",
|
||||
group(
|
||||
ArgGroup::new("edit_indexer")
|
||||
.args([
|
||||
"name",
|
||||
"enable_rss",
|
||||
"disable_rss",
|
||||
"enable_automatic_search",
|
||||
"disable_automatic_search",
|
||||
"enable_interactive_search",
|
||||
"disable_interactive_search",
|
||||
"url",
|
||||
"api_key",
|
||||
"seed_ratio",
|
||||
"tag",
|
||||
"priority",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Indexer {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the indexer whose settings you wish to edit",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
#[arg(long, help = "The name of the indexer")]
|
||||
name: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Readarr that this indexer should be used when Readarr periodically looks for releases via RSS Sync",
|
||||
conflicts_with = "disable_rss"
|
||||
)]
|
||||
enable_rss: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer when Readarr periodically looks for releases via RSS Sync",
|
||||
conflicts_with = "enable_rss"
|
||||
)]
|
||||
disable_rss: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Readarr that this indexer should be used when automatic searches are performed via the UI or by Readarr",
|
||||
conflicts_with = "disable_automatic_search"
|
||||
)]
|
||||
enable_automatic_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Readarr",
|
||||
conflicts_with = "enable_automatic_search"
|
||||
)]
|
||||
disable_automatic_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Readarr that this indexer should be used when an interactive search is used",
|
||||
conflicts_with = "disable_interactive_search"
|
||||
)]
|
||||
enable_interactive_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer whenever an interactive search is performed",
|
||||
conflicts_with = "enable_interactive_search"
|
||||
)]
|
||||
disable_interactive_search: bool,
|
||||
#[arg(long, help = "The URL of the indexer")]
|
||||
url: Option<String>,
|
||||
#[arg(long, help = "The API key used to access the indexer's API")]
|
||||
api_key: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
|
||||
)]
|
||||
seed_ratio: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Only use this indexer for authors with at least one matching tag ID. Leave blank to use with all authors.",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Readarr will still use all enabled indexers for RSS Sync and Searching"
|
||||
)]
|
||||
priority: Option<i64>,
|
||||
#[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrEditCommand> for Command {
|
||||
fn from(value: ReadarrEditCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Edit(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrEditCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrEditCommand> for ReadarrEditCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrEditCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrEditCommand::AllIndexerSettings {
|
||||
maximum_size,
|
||||
minimum_age,
|
||||
retention,
|
||||
rss_sync_interval,
|
||||
} => {
|
||||
if let Serdeable::Readarr(ReadarrSerdeable::IndexerSettings(previous_indexer_settings)) =
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAllIndexerSettings.into())
|
||||
.await?
|
||||
{
|
||||
let params = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
|
||||
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
|
||||
retention: retention.unwrap_or(previous_indexer_settings.retention),
|
||||
rss_sync_interval: rss_sync_interval
|
||||
.unwrap_or(previous_indexer_settings.rss_sync_interval),
|
||||
};
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::EditAllIndexerSettings(params).into())
|
||||
.await?;
|
||||
"All indexer settings updated".to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
ReadarrEditCommand::Author {
|
||||
author_id,
|
||||
enable_monitoring,
|
||||
disable_monitoring,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tag,
|
||||
clear_tags,
|
||||
} => {
|
||||
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
|
||||
let edit_author_params = EditAuthorParams {
|
||||
author_id,
|
||||
monitored: monitored_value,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tags: tag,
|
||||
tag_input_string: None,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::EditAuthor(edit_author_params).into())
|
||||
.await?;
|
||||
"Author updated".to_owned()
|
||||
}
|
||||
ReadarrEditCommand::Indexer {
|
||||
indexer_id,
|
||||
name,
|
||||
enable_rss,
|
||||
disable_rss,
|
||||
enable_automatic_search,
|
||||
disable_automatic_search,
|
||||
enable_interactive_search,
|
||||
disable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tag,
|
||||
priority,
|
||||
clear_tags,
|
||||
} => {
|
||||
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
|
||||
let automatic_search_value =
|
||||
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
|
||||
let interactive_search_value =
|
||||
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
|
||||
let edit_indexer_params = EditIndexerParams {
|
||||
indexer_id,
|
||||
name,
|
||||
enable_rss: rss_value,
|
||||
enable_automatic_search: automatic_search_value,
|
||||
enable_interactive_search: interactive_search_value,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tags: tag,
|
||||
tag_input_string: None,
|
||||
priority,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::EditIndexer(edit_indexer_params).into())
|
||||
.await?;
|
||||
"Indexer updated".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
use super::ReadarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "get_command_handler_tests.rs"]
|
||||
mod get_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrGetCommand {
|
||||
#[command(about = "Get the shared settings for all indexers")]
|
||||
AllIndexerSettings,
|
||||
#[command(about = "Get detailed information for the author with the given ID")]
|
||||
AuthorDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Get detailed information for the book with the given ID")]
|
||||
BookDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch the host config for your Readarr instance")]
|
||||
HostConfig,
|
||||
#[command(about = "Fetch the security config for your Readarr instance")]
|
||||
SecurityConfig,
|
||||
#[command(about = "Get the system status")]
|
||||
SystemStatus,
|
||||
}
|
||||
|
||||
impl From<ReadarrGetCommand> for Command {
|
||||
fn from(value: ReadarrGetCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Get(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrGetCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrGetCommand> for ReadarrGetCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrGetCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrGetCommand::AllIndexerSettings => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAllIndexerSettings.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::AuthorDetails { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAuthorDetails(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::BookDetails { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookDetails(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::HostConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetHostConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::SecurityConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetSecurityConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::SystemStatus => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetStatus.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, get_command_handler::ReadarrGetCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_get_command_from() {
|
||||
let command = ReadarrGetCommand::SystemStatus;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::Get(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_all_indexer_settings_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "all-indexer-settings"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "author-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"get",
|
||||
"author-details",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_details_requires_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "book-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"get",
|
||||
"book-details",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_get_commands_have_no_arg_requirements(
|
||||
#[values("host-config", "security-config", "system-status")] subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "get", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
readarr::get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler},
|
||||
},
|
||||
models::{Serdeable, readarr_models::ReadarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_all_indexer_settings_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_all_indexer_settings_command = ReadarrGetCommand::AllIndexerSettings;
|
||||
|
||||
let result = ReadarrGetCommandHandler::with(
|
||||
&app_arc,
|
||||
get_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_author_details_command() {
|
||||
let expected_author_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAuthorDetails(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_author_details_command = ReadarrGetCommand::AuthorDetails { author_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_author_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_book_details_command() {
|
||||
let expected_book_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookDetails(expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_book_details_command = ReadarrGetCommand::BookDetails { book_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_book_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_host_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetHostConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_host_config_command = ReadarrGetCommand::HostConfig;
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_security_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetSecurityConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_security_config_command = ReadarrGetCommand::SecurityConfig;
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_system_status_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_system_status_command = ReadarrGetCommand::SystemStatus;
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::ReadarrCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "list_command_handler_tests.rs"]
|
||||
mod list_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrListCommand {
|
||||
#[command(about = "Fetch all history events for the author with the given ID")]
|
||||
AuthorHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Trigger a manual search for releases of the author with the given ID")]
|
||||
AuthorReleases {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author whose releases you wish to search for",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "List all authors in your Readarr library")]
|
||||
Authors,
|
||||
#[command(about = "List all items in the Readarr blocklist")]
|
||||
Blocklist,
|
||||
#[command(about = "List all editions for the book with the given ID")]
|
||||
BookEditions {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose editions you want to list",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "List all book files for the book with the given ID")]
|
||||
BookFiles {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose book files you want to list",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch all history events for the book with the given ID")]
|
||||
BookHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author who wrote the book whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "List all books for the author with the given ID")]
|
||||
Books {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author whose books you want to list",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "List disk space details for all provisioned root folders in Readarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "List all active downloads in Readarr")]
|
||||
Downloads {
|
||||
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
|
||||
count: u64,
|
||||
},
|
||||
#[command(about = "Fetch all Readarr history events")]
|
||||
History {
|
||||
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
},
|
||||
#[command(about = "List all Readarr indexers")]
|
||||
Indexers,
|
||||
#[command(about = "Fetch Readarr logs")]
|
||||
Logs {
|
||||
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Output the logs in the same format as they appear in the log files"
|
||||
)]
|
||||
output_in_log_format: bool,
|
||||
},
|
||||
#[command(about = "List all Readarr metadata profiles")]
|
||||
MetadataProfiles,
|
||||
#[command(about = "List all Readarr quality profiles")]
|
||||
QualityProfiles,
|
||||
#[command(about = "List all queued events")]
|
||||
QueuedEvents,
|
||||
#[command(about = "Trigger a manual search for releases of the book with the given ID")]
|
||||
Releases {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose releases you wish to search for",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "List all root folders in Readarr")]
|
||||
RootFolders,
|
||||
#[command(about = "List all Readarr tags")]
|
||||
Tags,
|
||||
#[command(about = "List all Readarr tasks")]
|
||||
Tasks,
|
||||
#[command(about = "List all Readarr updates")]
|
||||
Updates,
|
||||
}
|
||||
|
||||
impl From<ReadarrListCommand> for Command {
|
||||
fn from(value: ReadarrListCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::List(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrListCommandHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrListCommand> for ReadarrListCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrListCommandHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrListCommand::AuthorHistory { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAuthorHistory(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::AuthorReleases { author_id } => {
|
||||
println!("Searching for author releases. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAuthorReleases(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Authors => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::ListAuthors.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Blocklist => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::BookEditions { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookEditions(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::BookFiles { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookFiles(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::BookHistory { author_id, book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookHistory(author_id, book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Books { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBooks(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::DiskSpace => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetDiskSpace.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Downloads { count } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetDownloads(count).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::History { events } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetHistory(events).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Indexers => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Logs {
|
||||
events,
|
||||
output_in_log_format,
|
||||
} => {
|
||||
let logs = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetLogs(events).into())
|
||||
.await?;
|
||||
|
||||
if output_in_log_format {
|
||||
let log_lines = &self.app.lock().await.data.readarr_data.logs.items;
|
||||
|
||||
serde_json::to_string_pretty(log_lines)?
|
||||
} else {
|
||||
serde_json::to_string_pretty(&logs)?
|
||||
}
|
||||
}
|
||||
ReadarrListCommand::MetadataProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetMetadataProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::QualityProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetQualityProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::QueuedEvents => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetQueuedEvents.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Releases { book_id } => {
|
||||
println!("Searching for book releases. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookReleases(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::RootFolders => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetRootFolders.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Tags => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetTags.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Tasks => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetTasks.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Updates => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetUpdates.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,958 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, list_command_handler::ReadarrListCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_list_command_from() {
|
||||
let command = ReadarrListCommand::DiskSpace;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::List(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_list_commands_have_no_arg_requirements(
|
||||
#[values(
|
||||
"authors",
|
||||
"blocklist",
|
||||
"disk-space",
|
||||
"indexers",
|
||||
"metadata-profiles",
|
||||
"quality-profiles",
|
||||
"queued-events",
|
||||
"root-folders",
|
||||
"tags",
|
||||
"tasks",
|
||||
"updates"
|
||||
)]
|
||||
subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "logs", "--events"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_downloads_count_flag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"downloads",
|
||||
"--count",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "history", "--events"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_author_history_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "author-history"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_author_history_success() {
|
||||
let expected_args = ReadarrListCommand::AuthorHistory { author_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"author-history",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(author_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(author_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_books_requires_author_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "books"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_editions_requires_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-editions"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_editions_success() {
|
||||
let expected_args = ReadarrListCommand::BookEditions { book_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"book-editions",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(book_editions_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(book_editions_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_files_requires_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-files"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_files_success() {
|
||||
let expected_args = ReadarrListCommand::BookFiles { book_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"book-files",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(book_files_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(book_files_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_history_requires_author_id_and_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-history"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_history_success() {
|
||||
let expected_args = ReadarrListCommand::BookHistory {
|
||||
author_id: 1,
|
||||
book_id: 2,
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"book-history",
|
||||
"--author-id",
|
||||
"1",
|
||||
"--book-id",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(book_history_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(book_history_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_books_success() {
|
||||
let expected_args = ReadarrListCommand::Books { author_id: 1 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "readarr", "list", "books", "--author-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(books_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(books_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_releases_requires_book_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "releases"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_author_releases_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "author-releases"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_author_releases_success() {
|
||||
let expected_args = ReadarrListCommand::AuthorReleases { author_id: 3 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"author-releases",
|
||||
"--author-id",
|
||||
"3",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(author_releases_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(author_releases_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_releases_success() {
|
||||
let expected_args = ReadarrListCommand::Releases { book_id: 7 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "readarr", "list", "releases", "--book-id", "7"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(releases_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(releases_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_default_values() {
|
||||
let expected_args = ReadarrListCommand::Logs {
|
||||
events: 500,
|
||||
output_in_log_format: false,
|
||||
};
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "list", "logs"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(logs_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(logs_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_downloads_default_values() {
|
||||
let expected_args = ReadarrListCommand::Downloads { count: 500 };
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "list", "downloads"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(downloads_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(downloads_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_downloads_success() {
|
||||
let expected_args = ReadarrListCommand::Downloads { count: 1000 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"downloads",
|
||||
"--count",
|
||||
"1000",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(downloads_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(downloads_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_default_values() {
|
||||
let expected_args = ReadarrListCommand::History { events: 500 };
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "list", "history"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(history_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(history_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_success() {
|
||||
let expected_args = ReadarrListCommand::History { events: 1000 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "readarr", "list", "history", "--events", "1000"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(history_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(history_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
readarr::list_command_handler::{ReadarrListCommand, ReadarrListCommandHandler},
|
||||
},
|
||||
models::{HorizontallyScrollableText, Serdeable, readarr_models::ReadarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_authors_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::ListAuthors.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_authors_command = ReadarrListCommand::Authors;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_authors_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_blocklist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_blocklist_command = ReadarrListCommand::Blocklist;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_blocklist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_author_history_command() {
|
||||
let expected_author_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAuthorHistory(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_author_history_command = ReadarrListCommand::AuthorHistory { author_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_author_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_books_command() {
|
||||
let expected_author_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBooks(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_books_command = ReadarrListCommand::Books { author_id: 1 };
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_books_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_book_editions_command() {
|
||||
let expected_book_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookEditions(expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_book_editions_command = ReadarrListCommand::BookEditions { book_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_book_editions_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_book_files_command() {
|
||||
let expected_book_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookFiles(expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_book_files_command = ReadarrListCommand::BookFiles { book_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_book_files_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_book_history_command() {
|
||||
let expected_author_id = 1;
|
||||
let expected_book_id = 2;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookHistory(expected_author_id, expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_book_history_command = ReadarrListCommand::BookHistory {
|
||||
author_id: 1,
|
||||
book_id: 2,
|
||||
};
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_book_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_disk_space_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetDiskSpace.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_disk_space_command = ReadarrListCommand::DiskSpace;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_disk_space_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_downloads_command() {
|
||||
let expected_count = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetDownloads(expected_count).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_downloads_command = ReadarrListCommand::Downloads { count: 1000 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_history_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetHistory(expected_events).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_history_command = ReadarrListCommand::History { events: 1000 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_indexers_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetIndexers.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_indexers_command = ReadarrListCommand::Indexers;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_indexers_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_logs_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetLogs(expected_events).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_logs_command = ReadarrListCommand::Logs {
|
||||
events: 1000,
|
||||
output_in_log_format: false,
|
||||
};
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_logs_command_output_in_log_format() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetLogs(1000).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.readarr_data
|
||||
.logs
|
||||
.set_items(vec![HorizontallyScrollableText::from("readarr log line")]);
|
||||
let list_logs_command = ReadarrListCommand::Logs {
|
||||
events: 1000,
|
||||
output_in_log_format: true,
|
||||
};
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(result.unwrap(), "[\n \"readarr log line\"\n]");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_metadata_profiles_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetMetadataProfiles.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_metadata_profiles_command = ReadarrListCommand::MetadataProfiles;
|
||||
|
||||
let result = ReadarrListCommandHandler::with(
|
||||
&app_arc,
|
||||
list_metadata_profiles_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_quality_profiles_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetQualityProfiles.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_quality_profiles_command = ReadarrListCommand::QualityProfiles;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_quality_profiles_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_queued_events_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetQueuedEvents.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_queued_events_command = ReadarrListCommand::QueuedEvents;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_queued_events_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_releases_command() {
|
||||
let expected_book_id = 7;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookReleases(expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_releases_command = ReadarrListCommand::Releases { book_id: 7 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_releases_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_author_releases_command() {
|
||||
let expected_author_id = 3;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAuthorReleases(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_author_releases_command = ReadarrListCommand::AuthorReleases { author_id: 3 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_author_releases_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_root_folders_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetRootFolders.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_root_folders_command = ReadarrListCommand::RootFolders;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_root_folders_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_tags_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetTags.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_tags_command = ReadarrListCommand::Tags;
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_tags_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_tasks_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetTasks.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_tasks_command = ReadarrListCommand::Tasks;
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_tasks_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_updates_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetUpdates.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_updates_command = ReadarrListCommand::Updates;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_updates_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use add_command_handler::{ReadarrAddCommand, ReadarrAddCommandHandler};
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use delete_command_handler::{ReadarrDeleteCommand, ReadarrDeleteCommandHandler};
|
||||
use edit_command_handler::{ReadarrEditCommand, ReadarrEditCommandHandler};
|
||||
use get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler};
|
||||
use list_command_handler::{ReadarrListCommand, ReadarrListCommandHandler};
|
||||
use refresh_command_handler::{ReadarrRefreshCommand, ReadarrRefreshCommandHandler};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use trigger_automatic_search_command_handler::{
|
||||
ReadarrTriggerAutomaticSearchCommand, ReadarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
|
||||
use super::{CliCommandHandler, Command};
|
||||
use crate::models::readarr_models::ReadarrTaskName;
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
mod add_command_handler;
|
||||
mod delete_command_handler;
|
||||
mod edit_command_handler;
|
||||
mod get_command_handler;
|
||||
mod list_command_handler;
|
||||
mod refresh_command_handler;
|
||||
mod trigger_automatic_search_command_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "readarr_command_tests.rs"]
|
||||
mod readarr_command_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrCommand {
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to add or create new resources within your Readarr instance"
|
||||
)]
|
||||
Add(ReadarrAddCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to delete resources from your Readarr instance"
|
||||
)]
|
||||
Delete(ReadarrDeleteCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to edit resources in your Readarr instance"
|
||||
)]
|
||||
Edit(ReadarrEditCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to fetch details of the resources in your Readarr instance"
|
||||
)]
|
||||
Get(ReadarrGetCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to list attributes from your Readarr instance"
|
||||
)]
|
||||
List(ReadarrListCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to refresh the data in your Readarr instance"
|
||||
)]
|
||||
Refresh(ReadarrRefreshCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to trigger automatic searches for releases of different resources in your Readarr instance"
|
||||
)]
|
||||
TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand),
|
||||
#[command(about = "Clear the Readarr blocklist")]
|
||||
ClearBlocklist,
|
||||
#[command(about = "Manually download the given release")]
|
||||
DownloadRelease {
|
||||
#[arg(long, help = "The GUID of the release to download", required = true)]
|
||||
guid: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The indexer ID to download the release from",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Mark the Readarr history item with the given ID as 'failed'")]
|
||||
MarkHistoryItemAsFailed {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the history item you wish to mark as 'failed'",
|
||||
required = true
|
||||
)]
|
||||
history_item_id: i64,
|
||||
},
|
||||
#[command(about = "Search for a new author to add to Readarr")]
|
||||
SearchNewAuthor {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The name of the author you want to search for",
|
||||
required = true
|
||||
)]
|
||||
query: String,
|
||||
},
|
||||
#[command(about = "Start the specified Readarr task")]
|
||||
StartTask {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The name of the task to trigger",
|
||||
value_enum,
|
||||
required = true
|
||||
)]
|
||||
task_name: ReadarrTaskName,
|
||||
},
|
||||
#[command(about = "Test all Readarr indexers")]
|
||||
TestAllIndexers,
|
||||
#[command(
|
||||
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
|
||||
)]
|
||||
TestIndexer {
|
||||
#[arg(long, help = "The ID of the indexer to test", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Toggle monitoring for the specified author corresponding to the given author ID"
|
||||
)]
|
||||
ToggleAuthorMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Toggle monitoring for the specified book corresponding to the given book ID")]
|
||||
ToggleBookMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrCommand> for Command {
|
||||
fn from(readarr_command: ReadarrCommand) -> Command {
|
||||
Command::Readarr(readarr_command)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrCliHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrCliHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrCommand::Add(add_command) => {
|
||||
ReadarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::Delete(delete_command) => {
|
||||
ReadarrDeleteCommandHandler::with(self.app, delete_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::Edit(edit_command) => {
|
||||
ReadarrEditCommandHandler::with(self.app, edit_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::Get(get_command) => {
|
||||
ReadarrGetCommandHandler::with(self.app, get_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::List(list_command) => {
|
||||
ReadarrListCommandHandler::with(self.app, list_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::Refresh(refresh_command) => {
|
||||
ReadarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
|
||||
ReadarrTriggerAutomaticSearchCommandHandler::with(
|
||||
self.app,
|
||||
trigger_automatic_search_command,
|
||||
self.network,
|
||||
)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::ClearBlocklist => {
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::ClearBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::DownloadRelease { guid, indexer_id } => {
|
||||
let params = ReleaseDownloadBody { guid, indexer_id };
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DownloadRelease(params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
|
||||
let _ = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(
|
||||
&json!({"message": "Readarr history item marked as 'failed'"}),
|
||||
)?
|
||||
}
|
||||
ReadarrCommand::SearchNewAuthor { query } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::SearchNewAuthor(query).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::StartTask { task_name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::StartTask(task_name).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::TestAllIndexers => {
|
||||
println!("Testing all Readarr indexers. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::TestAllIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::TestIndexer { indexer_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::TestIndexer(indexer_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::ToggleAuthorMonitoring { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::ToggleAuthorMonitoring(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::ToggleBookMonitoring { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::ToggleBookMonitoring(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, list_command_handler::ReadarrListCommand},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_command_from() {
|
||||
let command = ReadarrCommand::List(ReadarrListCommand::Authors);
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(command));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use clap::CommandFactory;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::Cli;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_command_requires_a_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requires_history_item_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "mark-history-item-as-failed"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requires_guid() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"download-release",
|
||||
"--indexer-id",
|
||||
"6",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"download-release",
|
||||
"--guid",
|
||||
"test-release-guid",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"download-release",
|
||||
"--guid",
|
||||
"test-release-guid",
|
||||
"--indexer-id",
|
||||
"6",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"mark-history-item-as-failed",
|
||||
"--history-item-id",
|
||||
"1234",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_author_requires_query() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "search-new-author"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_author_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"search-new-author",
|
||||
"--query",
|
||||
"test query",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_requires_task_name() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "start-task"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_task_name_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"start-task",
|
||||
"--task-name",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"start-task",
|
||||
"--task-name",
|
||||
"check-health",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_indexer_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "test-indexer"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_indexer_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"test-indexer",
|
||||
"--indexer-id",
|
||||
"8",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_author_monitoring_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "toggle-author-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_author_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"toggle-author-monitoring",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_book_monitoring_requires_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "toggle-book-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_commands_that_have_no_arg_requirements(
|
||||
#[values("clear-blocklist", "test-all-indexers")] subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_book_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"toggle-book-monitoring",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::readarr::add_command_handler::ReadarrAddCommand;
|
||||
use crate::cli::readarr::edit_command_handler::ReadarrEditCommand;
|
||||
use crate::cli::readarr::get_command_handler::ReadarrGetCommand;
|
||||
use crate::cli::readarr::refresh_command_handler::ReadarrRefreshCommand;
|
||||
use crate::cli::readarr::trigger_automatic_search_command_handler::ReadarrTriggerAutomaticSearchCommand;
|
||||
use crate::models::readarr_models::{
|
||||
Author, BlocklistItem, BlocklistResponse, DeleteParams, ReadarrSerdeable, ReadarrTaskName,
|
||||
};
|
||||
use crate::models::servarr_models::{IndexerSettings, ReleaseDownloadBody};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
readarr::{
|
||||
ReadarrCliHandler, ReadarrCommand, delete_command_handler::ReadarrDeleteCommand,
|
||||
list_command_handler::ReadarrListCommand,
|
||||
},
|
||||
},
|
||||
models::Serdeable,
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_tag_command = ReadarrCommand::Add(ReadarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
});
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_system_status_command = ReadarrCommand::Get(ReadarrGetCommand::SystemStatus);
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
|
||||
let expected_delete_author_params = DeleteParams {
|
||||
id: 7,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DeleteAuthor(expected_delete_author_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_author_command = ReadarrCommand::Delete(ReadarrDeleteCommand::Author {
|
||||
author_id: 7,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: false,
|
||||
});
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, delete_author_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 26500,
|
||||
minimum_age: 17,
|
||||
retention: 43,
|
||||
rss_sync_interval: 35,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 9,
|
||||
maximum_size: 31200,
|
||||
minimum_age: 22,
|
||||
retention: 58,
|
||||
rss_sync_interval: 90,
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_all_indexer_settings_command =
|
||||
ReadarrCommand::Edit(ReadarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(26500),
|
||||
minimum_age: Some(17),
|
||||
retention: Some(43),
|
||||
rss_sync_interval: Some(35),
|
||||
});
|
||||
|
||||
let result = ReadarrCliHandler::with(
|
||||
&app_arc,
|
||||
edit_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::ListAuthors.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Authors(vec![
|
||||
Author::default(),
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_authors_command = ReadarrCommand::List(ReadarrListCommand::Authors);
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, list_authors_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateAllAuthors.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_all_authors_command = ReadarrCommand::Refresh(ReadarrRefreshCommand::AllAuthors);
|
||||
|
||||
let result =
|
||||
ReadarrCliHandler::with(&app_arc, refresh_all_authors_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
|
||||
{
|
||||
let expected_author_id = 7;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::TriggerAutomaticAuthorSearch(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
ReadarrCommand::TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand::Author {
|
||||
author_id: expected_author_id,
|
||||
});
|
||||
|
||||
let result = ReadarrCliHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_history_item_as_failed_command() {
|
||||
let expected_history_item_id = 1234i64;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let mark_history_item_as_failed_command = ReadarrCommand::MarkHistoryItemAsFailed {
|
||||
history_item_id: expected_history_item_id,
|
||||
};
|
||||
|
||||
let result = ReadarrCliHandler::with(
|
||||
&app_arc,
|
||||
mark_history_item_as_failed_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_release_command() {
|
||||
let expected_release_download_body = ReleaseDownloadBody {
|
||||
guid: "test-release-guid".to_owned(),
|
||||
indexer_id: 6,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let download_release_command = ReadarrCommand::DownloadRelease {
|
||||
guid: "test-release-guid".to_owned(),
|
||||
indexer_id: 6,
|
||||
};
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_new_author_command() {
|
||||
let expected_query = "test author".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::SearchNewAuthor(expected_query.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let search_new_author_command = ReadarrCommand::SearchNewAuthor {
|
||||
query: expected_query,
|
||||
};
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, search_new_author_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_start_task_command() {
|
||||
let expected_task_name = ReadarrTaskName::CheckHealth;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::StartTask(expected_task_name).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let start_task_command = ReadarrCommand::StartTask {
|
||||
task_name: ReadarrTaskName::CheckHealth,
|
||||
};
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_test_indexer_command() {
|
||||
let expected_indexer_id = 8;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::TestIndexer(expected_indexer_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let test_indexer_command = ReadarrCommand::TestIndexer { indexer_id: 8 };
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_test_all_indexers_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::TestAllIndexers.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let test_all_indexers_command = ReadarrCommand::TestAllIndexers;
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_toggle_author_monitoring_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::ToggleAuthorMonitoring(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let toggle_author_monitoring_command =
|
||||
ReadarrCommand::ToggleAuthorMonitoring { author_id: 1 };
|
||||
|
||||
let result = ReadarrCliHandler::with(
|
||||
&app_arc,
|
||||
toggle_author_monitoring_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_toggle_book_monitoring_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::ToggleBookMonitoring(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let toggle_book_monitoring_command = ReadarrCommand::ToggleBookMonitoring { book_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrCliHandler::with(&app_arc, toggle_book_monitoring_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_clear_blocklist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::BlocklistResponse(
|
||||
BlocklistResponse {
|
||||
records: vec![BlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let clear_blocklist_command = ReadarrCommand::ClearBlocklist;
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, clear_blocklist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
use super::ReadarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "refresh_command_handler_tests.rs"]
|
||||
mod refresh_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrRefreshCommand {
|
||||
#[command(about = "Refresh all author data for all authors in your Readarr library")]
|
||||
AllAuthors,
|
||||
#[command(about = "Refresh author data and scan disk for the author with the given ID")]
|
||||
Author {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the author to refresh information on and to scan the disk for",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Refresh all downloads in Readarr")]
|
||||
Downloads,
|
||||
}
|
||||
|
||||
impl From<ReadarrRefreshCommand> for Command {
|
||||
fn from(value: ReadarrRefreshCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Refresh(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrRefreshCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrRefreshCommand>
|
||||
for ReadarrRefreshCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrRefreshCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrRefreshCommand::AllAuthors => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::UpdateAllAuthors.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrRefreshCommand::Author { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::UpdateAndScanAuthor(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrRefreshCommand::Downloads => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::UpdateDownloads.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, refresh_command_handler::ReadarrRefreshCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_refresh_command_from() {
|
||||
let command = ReadarrRefreshCommand::Author { author_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::Refresh(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_refresh_all_authors_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "all-authors"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_downloads_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "downloads"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_author_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "author"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_author_with_author_id() {
|
||||
let expected_args = ReadarrRefreshCommand::Author { author_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"refresh",
|
||||
"author",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Readarr(ReadarrCommand::Refresh(refresh_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::readarr::refresh_command_handler::{
|
||||
ReadarrRefreshCommand, ReadarrRefreshCommandHandler,
|
||||
};
|
||||
use crate::{app::App, cli::CliCommandHandler};
|
||||
use crate::{
|
||||
models::{Serdeable, readarr_models::ReadarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_all_authors_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateAllAuthors.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = ReadarrRefreshCommand::AllAuthors;
|
||||
|
||||
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_author_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::UpdateAndScanAuthor(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = ReadarrRefreshCommand::Author { author_id: 1 };
|
||||
|
||||
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_downloads_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateDownloads.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = ReadarrRefreshCommand::Downloads;
|
||||
|
||||
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
use super::ReadarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "trigger_automatic_search_command_handler_tests.rs"]
|
||||
mod trigger_automatic_search_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrTriggerAutomaticSearchCommand {
|
||||
#[command(about = "Trigger an automatic search for the author with the specified ID")]
|
||||
Author {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the author you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Trigger an automatic search for the book with the specified ID")]
|
||||
Book {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the book you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrTriggerAutomaticSearchCommand> for Command {
|
||||
fn from(value: ReadarrTriggerAutomaticSearchCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrTriggerAutomaticSearchCommand>
|
||||
for ReadarrTriggerAutomaticSearchCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrTriggerAutomaticSearchCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrTriggerAutomaticSearchCommand::Author { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::TriggerAutomaticAuthorSearch(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrTriggerAutomaticSearchCommand::Book { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::TriggerAutomaticBookSearch(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{
|
||||
ReadarrCommand,
|
||||
trigger_automatic_search_command_handler::ReadarrTriggerAutomaticSearchCommand,
|
||||
},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_trigger_automatic_search_command_from() {
|
||||
let command = ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(command))
|
||||
);
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_author_search_requires_author_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"trigger-automatic-search",
|
||||
"author",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_author_search_with_author_id() {
|
||||
let expected_args = ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"trigger-automatic-search",
|
||||
"author",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(
|
||||
trigger_automatic_search_command,
|
||||
))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(trigger_automatic_search_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_book_search_requires_book_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"trigger-automatic-search",
|
||||
"book",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_book_search_with_book_id() {
|
||||
let expected_args = ReadarrTriggerAutomaticSearchCommand::Book { book_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"trigger-automatic-search",
|
||||
"book",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(
|
||||
trigger_automatic_search_command,
|
||||
))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(trigger_automatic_search_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::readarr::trigger_automatic_search_command_handler::{
|
||||
ReadarrTriggerAutomaticSearchCommand, ReadarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
use crate::{app::App, cli::CliCommandHandler};
|
||||
use crate::{
|
||||
models::{Serdeable, readarr_models::ReadarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_author_search_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::TriggerAutomaticAuthorSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
|
||||
|
||||
let result = ReadarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_book_search_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::TriggerAutomaticBookSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
ReadarrTriggerAutomaticSearchCommand::Book { book_id: 1 };
|
||||
|
||||
let result = ReadarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,12 +137,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
language_profile_id,
|
||||
series_type: series_type.to_string(),
|
||||
series_type,
|
||||
season_folder: !disable_season_folders,
|
||||
tags,
|
||||
tag_input_string: None,
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: monitor.to_string(),
|
||||
monitor,
|
||||
search_for_cutoff_unmet_episodes: !no_search_for_series,
|
||||
search_for_missing_episodes: !no_search_for_series,
|
||||
},
|
||||
|
||||
@@ -517,13 +517,13 @@ mod tests {
|
||||
root_folder_path: "/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
language_profile_id: 1,
|
||||
series_type: "anime".to_owned(),
|
||||
series_type: SeriesType::Anime,
|
||||
monitored: false,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
season_folder: false,
|
||||
add_options: AddSeriesOptions {
|
||||
monitor: "future".to_owned(),
|
||||
monitor: SeriesMonitor::Future,
|
||||
search_for_cutoff_unmet_episodes: false,
|
||||
search_for_missing_episodes: false,
|
||||
},
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::{
|
||||
cli::{CliCommandHandler, Command, mutex_flags_or_option},
|
||||
models::{
|
||||
Serdeable,
|
||||
servarr_models::EditIndexerParams,
|
||||
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
|
||||
servarr_models::{EditIndexerParams, IndexerSettings},
|
||||
sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable},
|
||||
},
|
||||
network::{NetworkTrait, sonarr_network::SonarrEvent},
|
||||
};
|
||||
|
||||
@@ -622,8 +622,8 @@ mod tests {
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
servarr_models::EditIndexerParams,
|
||||
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
|
||||
servarr_models::{EditIndexerParams, IndexerSettings},
|
||||
sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
|
||||
};
|
||||
@@ -632,10 +632,10 @@ mod tests {
|
||||
async fn test_handle_edit_all_indexer_settings_command() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
maximum_size: 12345,
|
||||
minimum_age: 13,
|
||||
retention: 21,
|
||||
rss_sync_interval: 60,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
@@ -647,11 +647,11 @@ mod tests {
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
id: 9,
|
||||
maximum_size: 22222,
|
||||
minimum_age: 33,
|
||||
retention: 47,
|
||||
rss_sync_interval: 90,
|
||||
},
|
||||
)))
|
||||
});
|
||||
@@ -668,10 +668,10 @@ mod tests {
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
maximum_size: Some(12345),
|
||||
minimum_age: Some(13),
|
||||
retention: Some(21),
|
||||
rss_sync_interval: Some(60),
|
||||
};
|
||||
|
||||
let result = SonarrEditCommandHandler::with(
|
||||
|
||||
@@ -249,7 +249,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
|
||||
} => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetSeasonHistory((series_id, season_number)).into())
|
||||
.handle_network_event(SonarrEvent::GetSeasonHistory(series_id, season_number).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
|
||||
@@ -543,7 +543,7 @@ mod tests {
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetSeasonHistory((expected_series_id, expected_season_number)).into(),
|
||||
SonarrEvent::GetSeasonHistory(expected_series_id, expected_season_number).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
|
||||
@@ -2,16 +2,18 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::SonarrCommand;
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::sonarr_models::{SonarrRelease, SonarrSerdeable};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, sonarr_network::SonarrEvent},
|
||||
};
|
||||
|
||||
use super::SonarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "manual_search_command_handler_tests.rs"]
|
||||
mod manual_search_command_handler_tests;
|
||||
@@ -28,7 +30,7 @@ pub enum SonarrManualSearchCommand {
|
||||
episode_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID.\nNote that when downloading a season release, ensure that the release includes 'fullSeason: true', otherwise you'll run into issues"
|
||||
about = "Trigger a manual search of full-season releases (full_season: true) for the given season corresponding to the series with the given ID"
|
||||
)]
|
||||
Season {
|
||||
#[arg(
|
||||
@@ -73,22 +75,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
|
||||
let result = match self.command {
|
||||
SonarrManualSearchCommand::Episode { episode_id } => {
|
||||
println!("Searching for episode releases. This may take a minute...");
|
||||
let resp = self
|
||||
match self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetEpisodeReleases(episode_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
.await
|
||||
{
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => {
|
||||
let seasons_vec: Vec<SonarrRelease> = releases_vec
|
||||
.into_iter()
|
||||
.filter(|release| !release.full_season)
|
||||
.collect();
|
||||
serde_json::to_string_pretty(&seasons_vec)?
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
_ => serde_json::to_string_pretty(&json!({"message": "Unexpected response format"}))?,
|
||||
}
|
||||
}
|
||||
SonarrManualSearchCommand::Season {
|
||||
series_id,
|
||||
season_number,
|
||||
} => {
|
||||
println!("Searching for season releases. This may take a minute...");
|
||||
let resp = self
|
||||
match self
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
.handle_network_event(SonarrEvent::GetSeasonReleases(series_id, season_number).into())
|
||||
.await
|
||||
{
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => {
|
||||
let seasons_vec: Vec<SonarrRelease> = releases_vec
|
||||
.into_iter()
|
||||
.filter(|release| release.full_season)
|
||||
.collect();
|
||||
serde_json::to_string_pretty(&seasons_vec)?
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
_ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -108,9 +108,13 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::models::sonarr_models::SonarrRelease;
|
||||
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
|
||||
torrent_release, usenet_release,
|
||||
};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
@@ -134,9 +138,13 @@ mod tests {
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![
|
||||
torrent_release(),
|
||||
SonarrRelease {
|
||||
full_season: true,
|
||||
..usenet_release()
|
||||
},
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 };
|
||||
@@ -150,23 +158,35 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&[torrent_release()]).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manual_season_search_command() {
|
||||
let expected_release = SonarrRelease {
|
||||
full_season: true,
|
||||
..usenet_release()
|
||||
};
|
||||
let expected_series_id = 1;
|
||||
let expected_season_number = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::GetSeasonReleases((expected_series_id, expected_season_number)).into(),
|
||||
SonarrEvent::GetSeasonReleases(expected_series_id, expected_season_number).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![
|
||||
torrent_release(),
|
||||
SonarrRelease {
|
||||
full_season: true,
|
||||
..usenet_release()
|
||||
},
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let manual_season_search_command = SonarrManualSearchCommand::Season {
|
||||
@@ -183,6 +203,10 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(
|
||||
result.unwrap(),
|
||||
serde_json::to_string_pretty(&[expected_release]).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler};
|
||||
use list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
|
||||
use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler};
|
||||
use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use trigger_automatic_search_command_handler::{
|
||||
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
|
||||
@@ -251,7 +252,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
|
||||
.network
|
||||
.handle_network_event(SonarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
|
||||
.await?;
|
||||
"Sonarr history item marked as 'failed'".to_owned()
|
||||
serde_json::to_string_pretty(&json!({"message": "Sonarr history item marked as 'failed'"}))?
|
||||
}
|
||||
SonarrCommand::SearchNewSeries { query } => {
|
||||
let resp = self
|
||||
@@ -296,7 +297,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(
|
||||
SonarrEvent::ToggleSeasonMonitoring((series_id, season_number)).into(),
|
||||
SonarrEvent::ToggleSeasonMonitoring(series_id, season_number).into(),
|
||||
)
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
|
||||
@@ -266,9 +266,10 @@ mod tests {
|
||||
},
|
||||
models::{
|
||||
Serdeable,
|
||||
servarr_models::IndexerSettings,
|
||||
sonarr_models::{
|
||||
BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody,
|
||||
SonarrSerdeable, SonarrTaskName,
|
||||
BlocklistItem, BlocklistResponse, Series, SonarrReleaseDownloadBody, SonarrSerdeable,
|
||||
SonarrTaskName,
|
||||
},
|
||||
},
|
||||
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
|
||||
@@ -754,7 +755,7 @@ mod tests {
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
SonarrEvent::ToggleSeasonMonitoring((expected_series_id, expected_season_number)).into(),
|
||||
SonarrEvent::ToggleSeasonMonitoring(expected_series_id, expected_season_number).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
|
||||