Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003f80098a
|
||
|
|
ad8a6743ab
|
||
|
|
c61404dc71
|
||
|
|
a5b95c165a
|
||
|
|
8ec4ec018b
|
||
|
|
642a5c0a87
|
||
|
|
9128e68d0a
|
||
|
|
ddc18ae21a
|
||
|
|
4b8b9cde57
|
||
|
|
238348f6ec
|
||
|
|
37f697d1b4
|
||
|
|
a70019b88f
|
||
|
|
fa02b70bf0 | ||
|
|
76f9f4da9a | ||
|
|
49be500b8c | ||
|
|
78918b6d88 | ||
|
|
9b98e53b55 | ||
|
|
b799fe3865 | ||
|
|
0e869e1e98 | ||
|
|
196bc55f35 | ||
|
|
e29bd67a3e | ||
|
|
424ed62229
|
||
|
|
ab7542651b
|
||
|
|
a222f40616
|
||
|
|
699e929ed2 | ||
|
|
5279d37ea2 | ||
|
|
a4b8a23424
|
||
|
|
f2ee1e71f6
|
||
|
|
bdeaf6ae35
|
||
|
|
35ed3f359d
|
||
|
|
040935178a
|
||
|
|
4f78a5940f
|
||
|
|
9822597b56
|
||
|
|
c42a46c7ca
|
||
|
|
1d936c5cb5
|
||
|
|
7b6d595397
|
||
|
|
25da032f02
|
||
|
|
e9c323c4e8
|
||
|
|
289abbd8eb
|
||
|
|
a68de41025
|
||
|
|
9d0973352f
|
||
|
|
76c490f45b
|
||
|
|
9b4b3003b5
|
||
|
|
d678f5777a
|
No files matched your search
@@ -1,11 +0,0 @@
|
||||
### AI assistance (if any):
|
||||
- List tools here and files touched by them
|
||||
|
||||
### Authorship & Understanding
|
||||
|
||||
- [ ] I wrote or heavily modified this code myself
|
||||
- [ ] I understand how it works end-to-end
|
||||
- [ ] I can maintain this code in the future
|
||||
- [ ] No undisclosed AI-generated code was used
|
||||
- [ ] If AI assistance was used, it is documented below
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
### Description
|
||||
<!-- What problem is this PR solving? -->
|
||||
|
||||
### Alternatives Considered (Optional)
|
||||
<!-- What alternatives did you consider? Why did you choose this approach? -->
|
||||
|
||||
### AI assistance (if any)
|
||||
<!-- Required if AI assistance was used to write or modify code in this PR. -->
|
||||
<!-- - List tools here -->
|
||||
|
||||
### Authorship & Understanding
|
||||
|
||||
- [ ] I wrote or heavily modified this code myself. If I didn't, I gave proper attribution to AI assistance above.
|
||||
- [ ] I understand how it works end-to-end
|
||||
- [ ] I can maintain this code in the future
|
||||
- [ ] No undisclosed AI-generated code was used
|
||||
- [ ] If AI assistance was used, it is documented above
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
name: 1.95.0 / check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install 1.95.0
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -93,8 +93,8 @@ act -W .github/workflows/release.yml --input_type bump=minor
|
||||
|
||||
## Authorship Policy
|
||||
|
||||
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
|
||||
Claude, etc.) is not permitted unless explicitly disclosed and approved.
|
||||
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.
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
|
||||
- [ ]  [Bazarr](https://www.bazarr.media/)
|
||||
- [ ]  [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:
|
||||
@@ -202,7 +207,23 @@ Key:
|
||||
|
||||
### Readarr
|
||||
|
||||
- [ ] Support for Readarr
|
||||
| TUI | CLI | Feature |
|
||||
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||
| ✅ | ✅ | View your library, downloads, blocklist, editions |
|
||||
| ✅ | ✅ | View details of a specific author, book, or edition including description, history, downloaded file info |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library |
|
||||
| ✅ | ✅ | Add authors to your library |
|
||||
| ✅ | ✅ | Delete authors, books, downloads, indexers, root folders, and book files |
|
||||
| ✅ | ✅ | Trigger automatic searches for authors or books |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for authors and downloads |
|
||||
| ✅ | ✅ | Manually search for author releases or books |
|
||||
| ✅ | ✅ | Edit your authors and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
|
||||
### Whisparr
|
||||
|
||||
@@ -245,7 +266,7 @@ To see all available commands, simply run `managarr --help`:
|
||||
|
||||
```shell
|
||||
$ managarr --help
|
||||
managarr 0.7.0
|
||||
managarr 0.8.0
|
||||
Alex Clarke <alex.j.tusa@gmail.com>
|
||||
|
||||
A TUI and CLI to manage your Servarrs
|
||||
@@ -443,6 +464,11 @@ lidarr:
|
||||
custom_headers: # Example of adding custom headers to all requests to the Servarr instance
|
||||
traefik-auth-bypass-key: someBypassKey1234567890
|
||||
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
|
||||
|
||||
readarr:
|
||||
- name: Chaptarr (Ebooks) # Example of how to use the Readarr support with a Readarr fork like Chaptarr
|
||||
uri: http://192.168.0.87:8789/readarr/gr/ebook
|
||||
api_token: someApiToken1234567890
|
||||
```
|
||||
|
||||
### SSL Configuration
|
||||
@@ -552,6 +578,46 @@ Managarr supports using environment variables on startup so you don't have to al
|
||||
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
|
||||
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
|
||||
|
||||
## Tested Readarr Forks
|
||||
|
||||
The following Readarr forks have been explicitly tested with Managarr:
|
||||
|
||||
* [Bookshelf](https://github.com/pennydreadful/bookshelf)
|
||||
* [Readarr-Resurrected](https://github.com/ricetim/readarr-rresurrected)
|
||||
* [Librarr](https://github.com/Rorqualx/Librarr)
|
||||
* [Chaptarr](https://github.com/Chaptarr/Chaptarr)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Chaptarr houses a dedicated Readarr facade middleware that activates on a path prefix, meaning a bare config doesn't
|
||||
> work. Instead, users need to configure both Audiobooks and Ebooks separately, and based on their desired metadata
|
||||
> fetcher (i.e. Goodreads and Hardcover IDs). Because of this, Chaptarr-based configurations must use the `uri` key
|
||||
> rather than the `host` and `port` keys. The below configuration file demonstrates all the supported variations for
|
||||
> connecting Managarr to Chaptarr.
|
||||
|
||||
### Chaptarr Example Configuration
|
||||
```yaml
|
||||
readarr:
|
||||
# Because Chaptarr supports both eBooks and audiobooks, the same instance must be provisioned twice if you're using
|
||||
# both so Managarr can differentiate between the two. Thus, for Chaptarr, you need an eBook server configured and an
|
||||
# audiobook server configured like below.
|
||||
|
||||
- name: eBooks (Goodreads) # Readarr originally used Goodreads as its metadata provider
|
||||
uri: http://192.168.0.87:8789/readarr/gr/ebook # Note the `gr` in the path prefix indicating "GoodReads"
|
||||
api_token: someApiToken1234567890
|
||||
|
||||
- name: Chaptarr Audiobooks (Goodreads)
|
||||
uri: http://192.168.0.105:8789/readarr/gr/audiobook
|
||||
api_token: someApiToken1234567890
|
||||
|
||||
- name: Chaptarr eBooks (Hardcover)
|
||||
uri: http://192.168.0.105:8789/readarr/hc/ebook # Note the `hc` in the path prefix indicating "HardCover"
|
||||
api_token: someApiToken1234567890
|
||||
|
||||
- name: Chaptarr Audiobooks (Hardcover)
|
||||
uri: http://192.168.0.105:8789/readarr/hc/audiobook
|
||||
api_token: someApiToken1234567890
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Radarr
|
||||
@@ -574,6 +640,13 @@ Managarr supports using environment variables on startup so you don't have to al
|
||||

|
||||

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

|
||||

|
||||

|
||||

|
||||

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

|
||||

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