Compare commits

...
39 Commits
Author SHA1 Message Date
Dark-Alex-17 21749a62ea fix: config-path correctly displays the config file used even if --config-file is passed
Check / stable / fmt (push) Failing after 27s
Check / beta / clippy (push) Failing after 1m11s
Check / stable / clippy (push) Failing after 1m11s
Check / nightly / doc (push) Successful in 1m1s
Check / 1.95.0 / check (push) Successful in 1m8s
Check / dockerfile / msrv (push) Successful in 5s
Test Suite / ubuntu / beta (push) Successful in 2m17s
Test Suite / ubuntu / stable (push) Successful in 2m16s
Test Suite / ubuntu / stable / coverage (push) Failing after 1m57s
Test Suite / macos-latest / stable (push) Canceled after 0s
Test Suite / windows-latest / stable (push) Canceled after 0s
2026-10-14 12:56:31 -06:00
Dark-Alex-17 947b3c2eda fix: only instantiate the reqwest client on paths that actually need it 2026-10-14 12:49:23 -06:00
github-actions[bot] 9992a2bff0 chore: bump Cargo.toml to 0.8.1
Check / stable / fmt (push) Failing after 24s
Check / beta / clippy (push) Failing after 1m10s
Check / stable / clippy (push) Failing after 1m10s
Check / nightly / doc (push) Successful in 1m1s
Check / 1.95.0 / check (push) Successful in 1m8s
Check / dockerfile / msrv (push) Successful in 5s
Test Suite / ubuntu / beta (push) Successful in 2m15s
Test Suite / ubuntu / stable (push) Successful in 2m16s
Test Suite / ubuntu / stable / coverage (push) Failing after 1m55s
Test Suite / macos-latest / stable (push) Canceled after 0s
Test Suite / windows-latest / stable (push) Canceled after 0s
2026-10-12 17:31:42 +00:00
github-actions[bot] c5ac78a310 bump: version 0.8.0 → 0.8.1 [skip ci] 2026-10-12 17:31:40 +00:00
Dark-Alex-17 0f230a1a4f fix: never panic when the log directory is unwritable; disable file logging instead [#67]
Check / stable / fmt (push) Failing after 26s
Check / beta / clippy (push) Failing after 1m10s
Check / stable / clippy (push) Failing after 1m10s
Check / nightly / doc (push) Successful in 1m0s
Check / 1.95.0 / check (push) Successful in 1m7s
Check / dockerfile / msrv (push) Successful in 5s
Test Suite / ubuntu / beta (push) Successful in 2m15s
Test Suite / ubuntu / stable (push) Successful in 2m14s
Test Suite / ubuntu / stable / coverage (push) Failing after 1m57s
Test Suite / macos-latest / stable (push) Canceled after 0s
Test Suite / windows-latest / stable (push) Canceled after 0s
2026-10-12 11:16:11 -06:00
Dark-Alex-17 f6071f73b5 Merge branch 'main' of github.com:Dark-Alex-17/managarr
Check / stable / fmt (push) Failing after 25s
Check / beta / clippy (push) Failing after 1m15s
Check / stable / clippy (push) Failing after 1m11s
Check / nightly / doc (push) Successful in 1m3s
Check / 1.95.0 / check (push) Successful in 1m10s
Check / dockerfile / msrv (push) Successful in 5s
Test Suite / ubuntu / beta (push) Successful in 2m16s
Test Suite / ubuntu / stable (push) Successful in 2m17s
Test Suite / ubuntu / stable / coverage (push) Failing after 2m0s
Test Suite / macos-latest / stable (push) Canceled after 0s
Test Suite / windows-latest / stable (push) Canceled after 0s
2026-10-01 14:45:21 -06:00
Dark-Alex-17 11524bb232 feat: all CLI-based errors now come out in the same JSON format to further improve scripting and LLM integration
Check / stable / fmt (push) Failing after 25s
Check / beta / clippy (push) Failing after 1m15s
Check / stable / clippy (push) Failing after 1m12s
Check / nightly / doc (push) Successful in 1m2s
Check / 1.95.0 / check (push) Successful in 1m8s
Check / dockerfile / msrv (push) Successful in 5s
Test Suite / ubuntu / beta (push) Successful in 2m21s
Test Suite / ubuntu / stable (push) Successful in 2m20s
Test Suite / ubuntu / stable / coverage (push) Failing after 1m59s
Test Suite / macos-latest / stable (push) Canceled after 0s
Test Suite / windows-latest / stable (push) Canceled after 0s
2026-09-30 22:02:46 -06:00
github-actions[bot] 72ace0f2d5 chore: bump Cargo.toml to 0.8.0 2026-09-30 17:57:08 +00:00
github-actions[bot] 9375e04ebf bump: version 0.7.3 → 0.8.0 [skip ci] 2026-09-30 17:57:06 +00:00
Dark-Alex-17 7a79e1c05e ci: Added a dockerfile check to ensure the dockerfile MSRV matches Cargo.toml
Check / stable / fmt (push) Failing after 32s
Check / beta / clippy (push) Failing after 1m20s
Check / stable / clippy (push) Failing after 1m19s
Check / nightly / doc (push) Successful in 1m4s
Check / 1.95.0 / check (push) Successful in 1m8s
Check / dockerfile / msrv (push) Successful in 5s
Test Suite / ubuntu / beta (push) Successful in 2m24s
Test Suite / ubuntu / stable (push) Successful in 2m28s
Test Suite / ubuntu / stable / coverage (push) Failing after 2m22s
Test Suite / macos-latest / stable (push) Canceled after 0s
Test Suite / windows-latest / stable (push) Canceled after 0s
2026-09-30 11:40:46 -06:00
Alex Clarke 06e504732d Merge pull request #65 from Dark-Alex-17/readarr-support
Readarr support
2026-09-30 10:41:44 -06:00
Dark-Alex-17 003f80098a docs: Updated the pull request template 2026-09-30 00:11:56 -06:00
Dark-Alex-17 ad8a6743ab docs: Added documentation on how to configure Chaptarr in Managarr 2026-09-30 00:03:29 -06:00
Dark-Alex-17 c61404dc71 docs: Updated the contribution guidelines and PR template to sound more like AI is allowed but only when disclosed 2026-09-29 23:46:40 -06:00
Dark-Alex-17 a5b95c165a fix(readarr): send an empty images list when adding an author 2026-09-29 23:36:11 -06:00
Dark-Alex-17 8ec4ec018b fix(readarr): accept release searches wrapped in a filter summary envelope 2026-09-29 23:30:30 -06:00
Dark-Alex-17 642a5c0a87 fix(readarr): tolerate book payloads that omit the grabbed flag 2026-09-29 23:25:19 -06:00
Dark-Alex-17 9128e68d0a test(readarr): cover the CLI handler dispatch to each subcommand handler 2026-09-29 16:52:53 -06:00
Dark-Alex-17 ddc18ae21a docs: Updated the README to mention that Readarr has been sunset but Managarr is compatible with any APIs that are compatible with Readarr v1 2026-09-29 15:54:47 -06:00
Dark-Alex-17 4b8b9cde57 docs: Updated README with screenshots of Readarr 2026-09-29 14:24:00 -06:00
Dark-Alex-17 238348f6ec fix(radarr): render the movie overview one size down so the collection details stay visible behind it 2026-09-29 13:49:28 -06:00
Dark-Alex-17 37f697d1b4 docs: reformatted tables in README 2026-09-29 13:47:04 -06:00
Dark-Alex-17 a70019b88f Merge remote-tracking branch 'origin/main' into readarr-support
# Conflicts:
#	src/utils_tests.rs
2026-09-29 12:46:54 -06:00
Alex Clarke fa02b70bf0 Merge pull request #64 from solareon/fix/servarr_name
fix: passes servarr-name global switch to both the tui and cli
2026-09-29 10:54:20 -06:00
Solareon 76f9f4da9a test: remove disable-spinner to improve test coverage 2026-09-29 11:05:37 +02:00
Solareon 49be500b8c style: cargo fmt and nested if fix 2026-09-29 10:44:39 +02:00
Solareon 78918b6d88 test: ensure no servarr-name select first configured instance 2026-09-29 10:40:11 +02:00
Solareon 9b98e53b55 test: servarr-name with whitespace is trimmed properly 2026-09-29 10:31:01 +02:00
Solareon b799fe3865 test: unknown servarr-name fails properly 2026-09-29 10:21:00 +02:00
Solareon 0e869e1e98 test: ensure duplicate servarr names select correct instance 2026-09-29 10:05:37 +02:00
Solareon 196bc55f35 test: validate selected servarr-name type mismatch 2026-09-29 10:00:24 +02:00
Solareon e29bd67a3e test: happy path test for servarr-name 2026-09-29 08:55:04 +02:00
Dark-Alex-17 424ed62229 chore: update dependencies ahead of release 2026-09-28 16:02:28 -06:00
Dark-Alex-17 ab7542651b chore: bump GitHub Actions to Node 24 runtimes 2026-09-28 15:53:26 -06:00
Dark-Alex-17 a222f40616 fmt: addressed linter complaints 2026-09-28 14:49:53 -06:00
solareon 699e929ed2 fix(app/mod): use correct api for tab when starting on non default tab
Further testing of the servarr_name switch revealed an issue where selecting a server that didn't match the first tab caused the wrong api to be used. Now when a non-default tab is selected the correct api is used
2026-09-25 07:44:51 -04:00
solareon 5279d37ea2 fix: passes servarr-name global switch to both the tui and cli
servarr-name global switch was documented but not fully implemented in commit fd6fcfc. this commit fully implements the feature
2026-09-25 07:30:01 -04:00
Dark-Alex-17 a4b8a23424 docs: list the Readarr features in the README 2026-09-19 13:57:49 -06:00
Dark-Alex-17 f2ee1e71f6 feat: show sizes under a gigabyte in megabytes
Every size cell converted bytes to gigabytes and formatted the float in
place, so anything below a gigabyte rendered as 0.00 GB. Books and
tracks are almost always that small, which made the Size columns in
Readarr and Lidarr useless.

Replace convert_to_gb and convert_f64_to_gb with one format_size helper
that renders gigabytes at or above 1 GiB and megabytes below it, at the
precision each site already used. Every render site across the four
servarr trees now calls it, including the two movie and episode detail
strings built on the network side. Values at or above a gigabyte render
byte for byte as before.

Give the Readarr book statistics and book file fixtures and the Lidarr
album statistics fixture realistic sub-gigabyte sizes so the megabyte
path is pinned by the committed snapshots.
2026-09-18 21:44:22 -06:00
161 changed files with 2904 additions and 1057 deletions

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
+18
View File
@@ -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
+32 -5
View File
@@ -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
@@ -88,3 +88,30 @@ jobs:
- name: cargo +1.95.0 check
run: cargo check
dockerfile-msrv:
# The Dockerfile's pinned Rust image is only built at release time, so a
# rust-version bump that misses it fails silently until the next release.
# Catch the drift on every push instead.
name: dockerfile / msrv
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Check Dockerfile Rust image against Cargo.toml rust-version
run: |
set -euo pipefail
msrv=$(sed -nE 's/^rust-version = "([0-9.]+)"$/\1/p' Cargo.toml)
image_tag=$(sed -nE 's/^FROM rust:([0-9.]+) AS builder$/\1/p' Dockerfile)
: "${msrv:?failed to extract rust-version from Cargo.toml}"
: "${image_tag:?failed to extract the rust image tag from the Dockerfile}"
# The rust image tag pins major.minor, so compare on major.minor only
msrv_mm=$(echo "$msrv" | cut -d. -f1,2)
image_mm=$(echo "$image_tag" | cut -d. -f1,2)
if [ "$(printf '%s\n' "$msrv_mm" "$image_mm" | sort -V | head -n1)" != "$msrv_mm" ]; then
echo "Dockerfile builds with rust:$image_tag, but Cargo.toml declares rust-version = \"$msrv\"."
echo "Cargo refuses to build under an older toolchain, so the release Docker publish step will fail."
echo "Update the FROM rust:* line in the Dockerfile to at least $msrv_mm."
exit 1
fi
echo "OK: Dockerfile rust:$image_tag satisfies rust-version $msrv"
+28 -28
View File
@@ -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') }}
+5 -5
View File
@@ -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 }}
+151
View File
@@ -5,6 +5,157 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v0.8.1 (2026-10-12)
### Feat
- all CLI-based errors now come out in the same JSON format to further improve scripting and LLM integration
### Fix
- never panic when the log directory is unwritable; disable file logging instead [#67]
## v0.8.0 (2026-09-30)
### Feat
- show sizes under a gigabyte in megabytes
- **lidarr**: highlight tracks whose album is downloading or awaiting import
- **readarr**: show the author overview in the add author prompt
- **radarr**: render the movie overview popup at the large size
- **radarr**: make the collection movie overview popup scrollable
- **sonarr**: add a scrollable overview popup to series details
- **lidarr**: add a scrollable overview popup to artist details
- **readarr**: add a scrollable overview popup to author details
- **readarr**: add the indexers UI
- **readarr**: add the indexers handlers
- **readarr**: add the system UI
- **readarr**: add the system handlers
- **readarr**: add the root folders UI
- **readarr**: add the add root folder handler
- **readarr**: add the root folders handler
- **readarr**: add the history UI
- **readarr**: add the downloads UI
- **readarr**: add the blocklist UI
- **readarr**: add the history handler
- **readarr**: add the downloads handler
- **readarr**: add the blocklist handler
- **readarr**: add the delete book UI
- **readarr**: add the delete author UI
- **readarr**: add the edit author UI
- **readarr**: add the add author UI
- **readarr**: add the delete book handler
- **readarr**: add the delete author handler
- **readarr**: add the edit author handler
- **readarr**: add the add author handler
- **readarr**: render the edition details popup
- **readarr**: render the book details popup
- **readarr**: reach the book file info tab from the book details tabs
- **readarr**: scroll and dismiss the edition details popup
- **readarr**: handle the book details tabs and open the edition details popup
- **readarr**: render the author details view
- **readarr**: handle key events for the author details view
- **readarr**: handle key events for the authors library table
- **readarr**: fetch data on tick for the active block
- **readarr**: edit the shared indexer settings and expose the command on the CLI
- **readarr**: fetch the shared indexer settings and expose them on the CLI
- **readarr**: test all indexers and expose the command on the CLI
- **readarr**: test an indexer and expose the command on the CLI
- **readarr**: delete an indexer and expose the command on the CLI
- **readarr**: edit an indexer and expose the command on the CLI
- **readarr**: fetch all indexers and expose them on the CLI
- **readarr**: clear the blocklist and expose the command on the CLI
- **readarr**: delete an item from the blocklist and expose the command on the CLI
- **readarr**: fetch the blocklist and expose it on the CLI
- **readarr**: manually download a release and expose the command on the CLI
- **readarr**: fetch releases for an author and expose them on the CLI
- **readarr**: fetch releases for a book and expose them on the CLI
- **readarr**: mark a history item as failed and expose the command on the CLI
- **readarr**: fetch Readarr history and expose it on the CLI
- **readarr**: refresh Readarr downloads and expose the command on the CLI
- **readarr**: delete a download and expose the command on the CLI
- **readarr**: fetch Readarr downloads and expose them on the CLI
- **readarr**: delete a book file and expose the command on the CLI
- **readarr**: delete a book and expose the command on the CLI
- **readarr**: toggle book monitoring and expose the command on the CLI
- **readarr**: trigger an automatic book search and expose the command on the CLI
- **readarr**: fetch Readarr book history and expose it on the CLI
- **readarr**: fetch Readarr book files and expose them on the CLI
- **readarr**: fetch Readarr book editions and expose them on the CLI
- **readarr**: fetch Readarr book details and expose them on the CLI
- **readarr**: fetch Readarr books and expose them on the CLI
- **readarr**: fetch Readarr author history and expose it on the CLI
- **readarr**: refresh all Readarr authors and expose the command on the CLI
- **readarr**: refresh and rescan a Readarr author and expose the command on the CLI
- **readarr**: trigger automatic author searches and expose the command on the CLI
- **readarr**: toggle Readarr author monitoring and expose the command on the CLI
- **readarr**: delete Readarr authors and expose the command on the CLI
- **readarr**: edit Readarr authors and expose the command on the CLI
- **readarr**: add authors to Readarr and expose the command on the CLI
- **readarr**: search for new Readarr authors and expose the command on the CLI
- **readarr**: fetch Readarr author details and expose them on the CLI
- **readarr**: fetch Readarr authors and expose them on the CLI
- **readarr**: delete Readarr root folders and expose the command on the CLI
- **readarr**: add Readarr root folders and expose the command on the CLI
- **readarr**: delete Readarr tags and expose the command on the CLI
- **readarr**: add Readarr tags and expose the command on the CLI
- **readarr**: fetch Readarr root folders and expose them on the CLI
- **readarr**: fetch Readarr tags and expose them on the CLI
- **readarr**: fetch Readarr metadata profiles and expose them on the CLI
- **readarr**: fetch Readarr quality profiles and expose them on the CLI
- **readarr**: fetch Readarr queued events and expose them on the CLI
- **readarr**: trigger Readarr tasks and expose the command on the CLI
- **readarr**: fetch Readarr tasks and expose them on the CLI
- **readarr**: fetch Readarr logs and expose them on the CLI
- **readarr**: fetch Readarr updates and expose them on the CLI
- **readarr**: fetch the Readarr security config and expose it on the CLI
- **readarr**: fetch the Readarr host config and expose it on the CLI
- **readarr**: fetch Readarr disk space and expose it on the CLI
- **readarr**: fetch the Readarr system status and expose it on the CLI
- **readarr**: add the Readarr health check network call
- scaffold Readarr support across the state, app, handler, UI, and CLI layers
- updated gitignore to not include coyote memory or workspace config
### Fix
- **readarr**: send an empty images list when adding an author
- **readarr**: accept release searches wrapped in a filter summary envelope
- **readarr**: tolerate book payloads that omit the grabbed flag
- **radarr**: render the movie overview one size down so the collection details stay visible behind it
- **app/mod**: use correct api for tab when starting on non default tab
- passes servarr-name global switch to both the tui and cli
- **radarr**: model the movie status as an enum
- page up and down stop at the table edges instead of wrapping
- **radarr**: serialize the credit type under the same key it is read from
- **radarr**: model the download status as a DownloadStatus enum
- fall back to the default when a servarr returns an unknown enum value
- show N/A instead of zeros when a season, album or book has no statistics
- stop paging up or down on a single row table from crashing
- keep history readable when an event type is unrecognised
- **readarr**: attach editions when toggling book monitoring
- **sonarr**: stop marking seasons fully downloaded when no episodes are known
- keep a space between overview words that span a line break
- parse the log level from the unscrolled log text
- **radarr**: show a loading block while the updates popup refreshes
- show confirmation prompt clues on delete prompts
- drop redundant references in format arguments
- **readarr**: stop a long author overview hiding the other fields
- **ui**: render detail popup labels in bold
- **readarr**: stop appending stray colons to the edition overview
- **readarr**: move the book file delete to the File tab
- **readarr**: render the update all authors confirmation prompt
- **readarr**: drop the always-empty author type column from the authors table
- tolerate update records that omit the changes list
- address reviewer feedback — rename scroll field to bool, relocate static, drop env var
- revert draw gating; replace frame-count scroll with wall-clock timer
- address reviewer feedback — regex OnceLock and restore tick rate
- reduce TUI idle CPU and improve server error banners
### Refactor
- share one DownloadStatus enum across the servarr models
- move shared servarr types into servarr_models
## v0.7.3 (2026-06-25)
### Feat
+2 -2
View File
@@ -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.
Generated
+567 -508
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "managarr"
version = "0.7.3"
version = "0.8.1"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
+123 -50
View File
@@ -26,6 +26,11 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
- [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/)
- [ ] ![tautulli_logo](logos/tautulli.png) [Tautulli](https://tautulli.com/)
> [!NOTE]
> The official Readarr repository has been archived, but Managarr's Readarr support is compatible with any Readarr forks
> that maintain compatibility with the official Readarr v1 API. See [Tested Readarr Forks](#tested-readarr-forks) below
> for forks that have been explicitly tested with Managarr, and any oddities in configuring them to work with Managarr.
## Try Out the Demo
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
Simply run the following command to start a demo:
@@ -134,8 +139,8 @@ Key:
| Symbol | Status |
|--------|-----------|
| ✅ | Supported |
| ❌ | Missing |
| ✅ | Supported |
| ❌ | Missing |
| 🕒 | Planned |
| 🚫 | Won't Add |
@@ -143,66 +148,82 @@ Key:
| TUI | CLI | Feature |
|-----|-----|----------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, collections, and blocklist |
| ✅ | ✅ | View details of a specific movie including description, history, downloaded file info, or the credits |
| ✅ | ✅ | View details of any collection and the movies in them |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library or collections |
| ✅ | ✅ | Add movies to your library |
| ✅ | ✅ | Delete movies, downloads, and indexers |
| ✅ | ✅ | Trigger automatic searches for movies |
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
| ✅ | ✅ | Manually search for movies |
| ✅ | ✅ | Edit your movies, collections, and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
| ✅ | ✅ | View your library, downloads, collections, and blocklist |
| ✅ | ✅ | View details of a specific movie including description, history, downloaded file info, or the credits |
| ✅ | ✅ | View details of any collection and the movies in them |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library or collections |
| ✅ | ✅ | Add movies to your library |
| ✅ | ✅ | Delete movies, downloads, and indexers |
| ✅ | ✅ | Trigger automatic searches for movies |
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
| ✅ | ✅ | Manually search for movies |
| ✅ | ✅ | Edit your movies, collections, and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
### Sonarr
| TUI | CLI | Feature |
|-----|-----|--------------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
### Lidarr
| TUI | CLI | Feature |
|-----|-----|----------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, tracks |
| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add artists to your library |
| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files |
| ✅ | ✅ | Trigger automatic searches for artists or albums |
| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads |
| ✅ | ✅ | Manually search for full artist discographies or albums |
| ✅ | ✅ | Edit your artists and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
| ✅ | ✅ | View your library, downloads, blocklist, tracks |
| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add artists to your library |
| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files |
| ✅ | ✅ | Trigger automatic searches for artists or albums |
| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads |
| ✅ | ✅ | Manually search for full artist discographies or albums |
| ✅ | ✅ | Edit your artists and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
### Readarr
- [ ] 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
![artist_discography_search](screenshots/lidarr/artist_discography_search.png)
![manual_album_search](screenshots/lidarr/manual_album_search.png)
### Readarr
![readarr_library](screenshots/readarr/readarr_library.png)
![author_details](screenshots/readarr/author_details.png)
![book_details](screenshots/readarr/book_details.png)
![manual_author_search](screenshots/readarr/manual_author_search.png)
![manual_album_search](screenshots/readarr/manual_book_search.png)
### General
![logs](screenshots/radarr/logs.png)
![indexers](screenshots/radarr/indexers.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

+31
View File
@@ -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 {
+5 -8
View File
@@ -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())
}
}
+44
View File
@@ -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);
}
}
+2 -2
View File
@@ -51,8 +51,8 @@ pub enum Command {
},
#[command(about = indoc!{"
Print the full path to the default configuration file.
This file can be changed to another location using the '--config-file' flag
Print the full path to the configuration file in use.
This is the default configuration file location unless overridden with the '--config-file' flag
"})]
ConfigPath,
}
+1
View File
@@ -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,
+244 -3
View File
@@ -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;
+1 -1
View File
@@ -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]
@@ -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,
@@ -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,
+35 -4
View File
@@ -131,7 +131,10 @@ async fn main() -> Result<()> {
let default_config_path = config_file_path.display().to_string();
if matches!(args.command, Some(Command::ConfigPath)) {
println!("{default_config_path}");
match args.global.config_file {
Some(ref config_file) => println!("{}", config_file.display()),
None => println!("{default_config_path}"),
}
return Ok(());
}
@@ -153,7 +156,6 @@ async fn main() -> Result<()> {
config.validate(&config_path);
config.post_process_initialization();
let reqwest_client = build_network_client(&config);
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
let cancellation_token = CancellationToken::new();
let ctrlc_cancellation_token = cancellation_token.clone();
@@ -171,13 +173,41 @@ 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(_) => {
let reqwest_client = build_network_client(&config);
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 } => {
@@ -192,6 +222,7 @@ async fn main() -> Result<()> {
}
},
None => {
let reqwest_client = build_network_client(&config);
let app_nw = Arc::clone(&app);
std::thread::spawn(move || {
start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client)
+26
View File
@@ -222,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,
@@ -280,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,
}
@@ -454,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 {
+40 -1
View File
@@ -9,7 +9,7 @@ mod tests {
AddAuthorSearchResult, BlocklistItem, BlocklistResponse, Book, BookFile, DownloadRecord,
DownloadsResponse, Edition, MediaInfo, MonitorType, NewItemMonitorType,
ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrHistoryWrapper, ReadarrRelease,
ReadarrTask, ReadarrTaskName,
ReadarrReleaseResponse, ReadarrTask, ReadarrTaskName,
};
use crate::models::servarr_models::{
DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log,
@@ -497,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 {
@@ -939,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!({
@@ -295,7 +295,7 @@ impl ReadarrData<'_> {
statistics: Some(BookStatistics {
book_file_count: 3,
total_book_count: 7,
size_on_disk: 2469606195,
size_on_disk: 2457600,
percent_of_books: 42.86,
}),
..Book::default()
@@ -538,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 {
@@ -34,7 +34,7 @@ mod tests {
"trackFileCount": 8,
"trackCount": 10,
"totalTrackCount": 10,
"sizeOnDisk": 1024,
"sizeOnDisk": 317162578,
"percentOfTracks": 99.9
}
}]);
@@ -74,7 +74,7 @@ pub mod test_utils {
"trackFileCount": 8,
"trackCount": 10,
"totalTrackCount": 10,
"sizeOnDisk": 1024,
"sizeOnDisk": 317162578,
"percentOfTracks": 99.9
}
}"#;
@@ -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,
}
}
+33 -3
View File
@@ -1,4 +1,5 @@
use std::fmt::Debug;
use std::error::Error;
use std::fmt::{self, Debug};
use std::sync::{Arc, OnceLock};
use anyhow::{Result, anyhow};
@@ -156,8 +157,22 @@ impl<'a, 'b> Network<'a, 'b> {
let error_body = format_error_body(&response_body);
error!("Request failed. Received {status} response code with body: {response_body}");
self.app.lock().await.handle_error(anyhow!("Request failed. Received {status} response code with body: {error_body}"));
Err(anyhow!("Request failed. Received {status} response code with body: {error_body}"))
let message = format!("Request failed. Received {status} response code with body: {error_body}");
let body = if response_body.is_empty() {
None
} else {
Some(
serde_json::from_str::<Value>(&response_body)
.unwrap_or(Value::String(response_body)),
)
};
let network_error = NetworkError {
message,
status: status.as_u16(),
body,
};
self.app.lock().await.handle_error(network_error.clone().into());
Err(network_error.into())
}
}
Err(e) => {
@@ -304,6 +319,21 @@ fn format_error_body(response_body: &str) -> String {
re.replace_all(&body, " ").to_string()
}
#[derive(Clone, Debug)]
pub struct NetworkError {
pub message: String,
pub status: u16,
pub body: Option<Value>,
}
impl fmt::Display for NetworkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for NetworkError {}
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq)]
pub enum RequestMethod {
Get,
+102 -2
View File
@@ -10,7 +10,7 @@ mod tests {
use reqwest::header::HeaderMap;
use rstest::rstest;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::{Value, json};
use tokio::sync::{Mutex, mpsc};
use tokio_util::sync::CancellationToken;
@@ -22,7 +22,9 @@ mod tests {
use crate::network::network_tests::test_utils::test_network;
use crate::network::radarr_network::RadarrEvent;
use crate::network::sonarr_network::SonarrEvent;
use crate::network::{Network, NetworkEvent, NetworkTrait, RequestMethod, RequestProps};
use crate::network::{
Network, NetworkError, NetworkEvent, NetworkTrait, RequestMethod, RequestProps,
};
#[tokio::test]
async fn test_handle_network_event_radarr_event() {
@@ -398,6 +400,104 @@ mod tests {
);
}
#[tokio::test]
async fn test_handle_request_non_success_code_downcasts_to_network_error_with_json_body() {
let (async_server, app_arc, server) = mock_api(RequestMethod::Get, 404, true).await;
let mut network = test_network(&app_arc);
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
method: RequestMethod::Get,
body: None,
api_token: "test1234".to_owned(),
ignore_status_code: false,
custom_headers: HeaderMap::new(),
},
|_, _| (),
)
.await;
async_server.assert_async().await;
let network_error = resp.unwrap_err().downcast::<NetworkError>().unwrap();
assert_str_eq!(
network_error.message,
r#"Request failed. Received 404 Not Found response code with body: { "value": "Test" }"#
);
assert_eq!(network_error.status, 404);
assert_some_eq_x!(network_error.body.as_ref(), &json!({ "value": "Test" }));
}
#[tokio::test]
async fn test_handle_request_non_success_code_downcasts_to_network_error_with_string_body() {
let mut server = Server::new_async().await;
let async_server = server
.mock("GET", "/test")
.match_header("X-Api-Key", "test1234")
.with_status(503)
.with_body("Service Unavailable")
.create_async()
.await;
let app_arc = Arc::new(Mutex::new(App::test_default()));
let mut network = test_network(&app_arc);
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
method: RequestMethod::Get,
body: None,
api_token: "test1234".to_owned(),
ignore_status_code: false,
custom_headers: HeaderMap::new(),
},
|_, _| (),
)
.await;
async_server.assert_async().await;
let network_error = resp.unwrap_err().downcast::<NetworkError>().unwrap();
assert_str_eq!(
network_error.message,
"Request failed. Received 503 Service Unavailable response code with body: Service Unavailable"
);
assert_eq!(network_error.status, 503);
assert_some_eq_x!(
network_error.body.as_ref(),
&Value::String("Service Unavailable".to_owned())
);
}
#[tokio::test]
async fn test_handle_request_non_success_code_downcasts_to_network_error_with_empty_body() {
let (async_server, app_arc, server) = mock_api(RequestMethod::Post, 404, false).await;
let mut network = test_network(&app_arc);
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
method: RequestMethod::Post,
body: None,
api_token: "test1234".to_owned(),
ignore_status_code: false,
custom_headers: HeaderMap::new(),
},
|_, _| (),
)
.await;
async_server.assert_async().await;
let network_error = resp.unwrap_err().downcast::<NetworkError>().unwrap();
assert_str_eq!(
network_error.message,
r#"Request failed. Received 404 Not Found response code with body: "#
);
assert_eq!(network_error.status, 404);
assert_none!(network_error.body);
}
#[rstest]
#[tokio::test]
async fn test_call_api(
+4 -4
View File
@@ -11,7 +11,7 @@ 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};
@@ -319,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
@@ -374,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: {}",
@@ -393,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,
@@ -759,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"
@@ -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()];
@@ -394,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,
@@ -8,7 +8,7 @@ use crate::models::sonarr_models::{
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,
+3 -3
View File
@@ -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()
+3 -3
View File
@@ -15,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};
@@ -377,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("")
@@ -408,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),
])
+9 -12
View File
@@ -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(),
]),
]);
}
@@ -282,13 +282,10 @@ fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|| "N/A".to_owned(),
|s| format!("{}/{}", s.track_file_count, s.total_track_count),
);
let size = album.statistics.as_ref().map_or_else(
|| "N/A".to_owned(),
|s| {
let size = convert_to_gb(s.size_on_disk);
format!("{size:.2} GB")
},
);
let size = album
.statistics
.as_ref()
.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(
@@ -500,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("")
@@ -531,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),
])
+3 -3
View File
@@ -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 ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
@@ -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
@@ -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
+3 -3
View File
@@ -37,7 +37,7 @@ use crate::{
servarr_data::lidarr::lidarr_data::LidarrData,
servarr_models::{DiskSpace, RootFolder},
},
utils::convert_to_gb,
utils::format_size,
};
mod blocklist;
@@ -176,8 +176,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let RootFolder {
path, free_space, ..
} = &monitored_root_folders[i];
let space: f64 = convert_to_gb(*free_space);
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free"))
let space = format_size(*free_space, 2);
let root_folder_space = Paragraph::new(format!("{path}: {space} free"))
.block(borderless_block())
.default_color();
+3 -3
View File
@@ -15,7 +15,7 @@ use crate::ui::utils::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_to_gb;
use crate::utils::format_size;
mod add_root_folder_ui;
@@ -69,11 +69,11 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
..
} = root_folders;
let space: f64 = convert_to_gb(*free_space);
let space = format_size(*free_space, 2);
Row::new(vec![
Cell::from(path.to_owned()),
Cell::from(format!("{space:.2} GB")),
Cell::from(space),
Cell::from(
unmapped_folders
.as_ref()
@@ -6,7 +6,7 @@ expression: output
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 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 │
│ │
│ │
│ │
@@ -46,7 +46,7 @@ impl DrawUi for CollectionDetailsUi {
draw_popup(f, app, draw_collection_details, Size::Large);
if context_option.unwrap_or(active_radarr_block) == ActiveRadarrBlock::ViewMovieOverview {
draw_popup(f, app, draw_movie_overview, Size::Large);
draw_popup(f, app, draw_movie_overview, Size::Medium);
}
}
}
@@ -8,40 +8,40 @@ expression: output
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Collection movie blah blah blah │
│ │
│It was shot in Madison, Wisconsin: a city the cast never really left. │
│ │
│The sequel took seven years to finish. │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Overview: Collection blah blah blah │
│Root Folder Path: /nfs/movies │
│Quality Profile: HD - 1080p │
│Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
│Monitored: Y│Collection movie blah blah blah │ │
│Search on Ad│ │ │
│ │It was shot in Madison, Wisconsin: a city the cast never really left. │ │
│ │ │ │
│ │The sequel took seven years to finish. │ │
│ Movies ──│ │───────────│
│ ✔ Title │ │ │
│=> ✔ Test │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ │
│ │
@@ -8,40 +8,40 @@ expression: output
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ │
│ Loading ... │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Overview: Collection blah blah blah │
│Root Folder Path: /nfs/movies │
│Quality Profile: HD - 1080p │
│Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
│Monitored: Y│ │ │
│Search on Ad│ │ │
│ │ Loading ... │ │
│ │ │ │
│ │ │ │
│ Movies ──│ │───────────│
│ │ │ │
│ │ │ │
│ Loading ...│ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ │
│ │
@@ -8,40 +8,40 @@ expression: output
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ │
│ Loading ... │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Overview: Collection blah blah blah │
│Root Folder Path: /nfs/movies │
│Quality Profile: HD - 1080p │
│Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
│Monitored: Y│ │ │
│Search on Ad│ │ │
│ │ Loading ... │ │
│ │ │ │
│ │ │ │
│ Movies ──│ │───────────│
│ ✔ Title │ │ │
│=> ✔ Test │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ │
│ │
@@ -8,40 +8,40 @@ expression: output
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│It was shot in Madison, Wisconsin: a city the cast never really left. │
│ │
│The sequel took seven years to finish. │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Overview: Collection blah blah blah │
│Root Folder Path: /nfs/movies │
│Quality Profile: HD - 1080p │
│Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
│Monitored: Y│It was shot in Madison, Wisconsin: a city the cast never really left. │ │
│Search on Ad│ │ │
│ │The sequel took seven years to finish. │ │
│ │ │ │
│ │ │ │
│ Movies ──│ │───────────│
│ ✔ Title │ │ │
│=> ✔ Test │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ │
│ │
+3 -3
View File
@@ -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_to_gb;
use crate::utils::format_size;
#[cfg(test)]
#[path = "downloads_ui_tests.rs"]
@@ -96,12 +96,12 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else {
1f64 - (*sizeleft as f64 / *size as f64)
};
let file_size: f64 = convert_to_gb(*size);
let file_size = format_size(*size, 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()
+3 -3
View File
@@ -16,7 +16,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_runtime, convert_to_gb};
use crate::utils::{convert_runtime, format_size};
mod add_movie_ui;
mod delete_movie_ui;
@@ -95,7 +95,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let monitored = if movie.monitored { "🏷" } else { "" };
let studio = movie.studio.clone().unwrap_or_default();
let (hours, minutes) = convert_runtime(movie.runtime);
let file_size: f64 = convert_to_gb(movie.size_on_disk);
let file_size = format_size(movie.size_on_disk, 2);
let certification = movie.certification.clone().unwrap_or_default();
let quality_profile = quality_profile_map
.get_by_left(&movie.quality_profile_id)
@@ -121,7 +121,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(format!("{hours}h {minutes}m")),
Cell::from(certification),
Cell::from(movie.original_language.name.to_owned()),
Cell::from(format!("{file_size:.2} GB")),
Cell::from(file_size),
Cell::from(quality_profile),
Cell::from(monitored.to_owned()),
Cell::from(tags),
+3 -3
View File
@@ -26,7 +26,7 @@ use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable;
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;
#[cfg(test)]
#[path = "movie_details_ui_tests.rs"]
@@ -398,7 +398,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
&& current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(),
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("")
@@ -434,7 +434,7 @@ fn draw_movie_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(language),
Cell::from(quality),
@@ -12,8 +12,8 @@ expression: output
│ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Language Quality │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 GB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 GB English HD - 1080p │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 MB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 MB English HD - 1080p │
│ │
│ │
│ │
@@ -12,8 +12,8 @@ expression: output
│ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Language Quality │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 GB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 GB English HD - 1080p │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 MB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 MB English HD - 1080p │
│ │
│ │
│ │
@@ -12,8 +12,8 @@ expression: output
│ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Age ⛔ Title Indexer Size Peers Language Quality │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 GB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 GB English HD - 1080p │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 MB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 MB English HD - 1080p │
│ │
│ │
│ │
@@ -12,8 +12,8 @@ expression: output
│ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Language Quality │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 GB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 GB English HD - 1080p │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 MB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 MB English HD - 1080p │
│ │
│ │
│ │
@@ -12,8 +12,8 @@ expression: output
│ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Language Quality │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 GB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 GB English HD - 1080p │
│=> torrent 12 days ⛔ Some movie release The Pirate Bay 0.0 MB 25 / 3 English HD - 1080p │
│ usenet 22 days ⛔ Some Other movie rele The Pirate Bay 0.0 MB English HD - 1080p │
│ │
│ │
│ │
+3 -3
View File
@@ -22,7 +22,7 @@ use crate::ui::utils::{
line_gauge_with_label, line_gauge_with_title, title_block,
};
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::utils::convert_to_gb;
use crate::utils::format_size;
use chrono::Duration;
#[cfg(not(test))]
use chrono::Utc;
@@ -170,8 +170,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let RootFolder {
path, free_space, ..
} = &monitored_root_folders[i];
let space: f64 = convert_to_gb(*free_space);
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free"))
let space = format_size(*free_space, 2);
let root_folder_space = Paragraph::new(format!("{path}: {space} free"))
.block(borderless_block())
.default_color();
+3 -3
View File
@@ -12,7 +12,7 @@ use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_input_box_popup, draw_popup};
use crate::utils::convert_to_gb;
use crate::utils::format_size;
#[cfg(test)]
#[path = "root_folders_ui_tests.rs"]
@@ -66,11 +66,11 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
..
} = root_folders;
let space: f64 = convert_to_gb(*free_space);
let space = format_size(*free_space, 2);
Row::new(vec![
Cell::from(path.to_owned()),
Cell::from(format!("{space:.2} GB")),
Cell::from(space),
Cell::from(
unmapped_folders
.as_ref()
+3 -3
View File
@@ -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()
+9 -12
View File
@@ -28,7 +28,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;
@@ -215,7 +215,7 @@ fn draw_author_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);
author_description.extend(vec![
Line::from(vec![
"Books: ".primary().bold(),
@@ -227,7 +227,7 @@ fn draw_author_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(),
]),
]);
}
@@ -278,13 +278,10 @@ fn draw_books_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|| "N/A".to_owned(),
|s| format!("{}/{}", s.book_file_count, s.total_book_count),
);
let size = book.statistics.as_ref().map_or_else(
|| "N/A".to_owned(),
|s| {
let size = convert_to_gb(s.size_on_disk);
format!("{size:.2} GB")
},
);
let size = book
.statistics
.as_ref()
.map_or_else(|| "N/A".to_owned(), |s| format_size(s.size_on_disk, 2));
decorate_book_row_with_style(
book,
@@ -483,7 +480,7 @@ fn draw_author_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
&& active_readarr_block != ActiveReadarrBlock::ManualAuthorSearchConfirmPrompt,
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("")
@@ -514,7 +511,7 @@ fn draw_author_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),
])
+5 -5
View File
@@ -14,7 +14,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};
@@ -234,7 +234,7 @@ fn draw_book_file_info(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
..
} = book_file;
let size = convert_to_gb(*size);
let size = format_size(*size, 2);
let audio_info = media_info
.as_ref()
.map(|media_info| {
@@ -249,7 +249,7 @@ fn draw_book_file_info(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Row::new(vec![
Cell::from(path.clone()),
Cell::from(format!("{size:.2} GB")),
Cell::from(size),
Cell::from(quality.quality.name.clone()),
Cell::from(audio_info),
])
@@ -395,7 +395,7 @@ fn draw_book_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
&& active_readarr_block != ActiveReadarrBlock::ManualBookSearchConfirmPrompt,
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("")
@@ -426,7 +426,7 @@ fn draw_book_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),
])
+3 -3
View File
@@ -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::{
@@ -103,7 +103,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let size = author
.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(&author.quality_profile_id)
.cloned()
@@ -134,7 +134,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(quality_profile),
Cell::from(metadata_profile),
Cell::from(books.to_string()),
Cell::from(format!("{size:.2} GB")),
Cell::from(format_size(size, 2)),
Cell::from(monitored.to_owned()),
Cell::from(tags),
]),
@@ -22,7 +22,7 @@ expression: output
││ Books │ History │ Author Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Files Release Date Size ││
││=> 🏷 Test Book 3/7 2023-05-20 2.30 GB ││
││=> 🏷 Test Book 3/7 2023-05-20 2.34 MB ││
││ ││
││ ││
││ ││
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff. Show more