Compare commits

...
Author SHA1 Message Date
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
155 changed files with 1893 additions and 984 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
+5 -5
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -46,7 +46,7 @@ jobs:
toolchain: [stable, beta] toolchain: [stable, beta]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -54,7 +54,7 @@ jobs:
components: clippy components: clippy
- name: Run clippy action - name: Run clippy action
uses: clechasseur/rs-clippy-check@v3 uses: clechasseur/rs-clippy-check@v5
- name: Cache Cargo dependencies - name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
@@ -65,7 +65,7 @@ jobs:
name: nightly / doc name: nightly / doc
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Rust nightly - name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@nightly
@@ -79,7 +79,7 @@ jobs:
name: 1.95.0 / check name: 1.95.0 / check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install 1.95.0 - name: Install 1.95.0
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
+28 -28
View File
@@ -32,19 +32,19 @@ jobs:
- name: Checkout repository - name: Checkout repository
if: env.ACT != 'true' if: env.ACT != 'true'
uses: actions/checkout@v3 uses: actions/checkout@v5
with: with:
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }} ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
fetch-depth: 0 fetch-depth: 0
- name: Checkout repository - name: Checkout repository
if: env.ACT == 'true' if: env.ACT == 'true'
uses: actions/checkout@v3 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: '3.10' python-version: '3.10'
@@ -219,13 +219,13 @@ jobs:
git push origin --follow-tags git push origin --follow-tags
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
path: artifacts path: artifacts
- name: Upload the changed Cargo files (Act) - name: Upload the changed Cargo files (Act)
if: env.ACT == 'true' if: env.ACT == 'true'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: bumped-cargo-files name: bumped-cargo-files
path: | path: |
@@ -267,7 +267,7 @@ jobs:
exit 1 exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 1 fetch-depth: 1
@@ -279,18 +279,18 @@ jobs:
- name: Get bumped Cargo files (Act) - name: Get bumped Cargo files (Act)
if: env.ACT == 'true' if: env.ACT == 'true'
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: bumped-cargo-files name: bumped-cargo-files
path: ${{ github.workspace }} path: ${{ github.workspace }}
- uses: actions/cache@v3 - uses: actions/cache@v5
name: Cache Cargo registry name: Cache Cargo registry
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
- uses: actions/cache@v3 - uses: actions/cache@v5
if: startsWith(matrix.job.name, 'linux-') if: startsWith(matrix.job.name, 'linux-')
with: with:
path: ~/.cargo/bin path: ~/.cargo/bin
@@ -375,7 +375,7 @@ jobs:
cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.sha256 artifacts/ cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.sha256 artifacts/
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: artifacts-${{ env.RELEASE_NAME }} name: artifacts-${{ env.RELEASE_NAME }}
path: artifacts path: artifacts
@@ -393,12 +393,12 @@ jobs:
exit 1 exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
path: artifacts path: artifacts
merge-multiple: true merge-multiple: true
@@ -421,7 +421,7 @@ jobs:
- name: Create a GitHub Release - name: Create a GitHub Release
if: env.ACT != 'true' if: env.ACT != 'true'
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v3
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -457,7 +457,7 @@ jobs:
prerelease: false prerelease: false
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
path: artifacts path: artifacts
overwrite: true overwrite: true
@@ -474,12 +474,12 @@ jobs:
exit 1 exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Get release artifacts - name: Get release artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
path: artifacts path: artifacts
merge-multiple: true merge-multiple: true
@@ -528,12 +528,12 @@ jobs:
exit 1 exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Get release artifacts - name: Get release artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
path: artifacts path: artifacts
merge-multiple: true merge-multiple: true
@@ -592,12 +592,12 @@ jobs:
exit 1 exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Get release artifacts - name: Get release artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
path: artifacts path: artifacts
merge-multiple: true merge-multiple: true
@@ -618,20 +618,20 @@ jobs:
echo "Release version: ${{ env.version }}" echo "Release version: ${{ env.version }}"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub - name: Login to Docker Hub
if: env.ACT != 'true' if: env.ACT != 'true'
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push to Docker Hub - name: Push to Docker Hub
uses: docker/build-push-action@v5 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: Dockerfile file: Dockerfile
@@ -651,13 +651,13 @@ jobs:
exit 1 exit 1
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get bumped Cargo files (Act) - name: Get bumped Cargo files (Act)
if: env.ACT == 'true' if: env.ACT == 'true'
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: bumped-cargo-files name: bumped-cargo-files
path: ${{ github.workspace }} path: ${{ github.workspace }}
@@ -668,13 +668,13 @@ jobs:
git fetch --all git fetch --all
git pull git pull
- uses: actions/cache@v3 - uses: actions/cache@v5
name: Cache Cargo registry name: Cache Cargo registry
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
- uses: actions/cache@v3 - uses: actions/cache@v5
with: with:
path: ~/.cargo/bin path: ~/.cargo/bin
key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }} key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }}
+5 -5
View File
@@ -33,7 +33,7 @@ jobs:
# toolchain # toolchain
toolchain: [stable, beta] toolchain: [stable, beta]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install ${{ matrix.toolchain }} - name: Install ${{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
@@ -74,7 +74,7 @@ jobs:
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# name: ubuntu / stable / minimal-versions # name: ubuntu / stable / minimal-versions
# steps: # steps:
# - uses: actions/checkout@v4 # - uses: actions/checkout@v5
# - name: Install Rust stable # - name: Install Rust stable
# uses: dtolnay/rust-toolchain@stable # uses: dtolnay/rust-toolchain@stable
@@ -110,7 +110,7 @@ jobs:
# - run: vcpkg install openssl:x64-windows-static-md # - run: vcpkg install openssl:x64-windows-static-md
# if: runner.os == 'Windows' # if: runner.os == 'Windows'
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -151,7 +151,7 @@ jobs:
name: ubuntu / stable / coverage name: ubuntu / stable / coverage
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -176,7 +176,7 @@ jobs:
- name: Upload to codecov.io - name: Upload to codecov.io
if: env.ACT != 'true' if: env.ACT != 'true'
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v7
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
+2 -2
View File
@@ -93,8 +93,8 @@ act -W .github/workflows/release.yml --input_type bump=minor
## Authorship Policy ## Authorship Policy
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT, All code in this repository is written and reviewed by humans, or AI-generated where explicitly disclosed. AI-generated code (e.g., Copilot, ChatGPT,
Claude, etc.) is not permitted unless explicitly disclosed and approved. Claude, etc.) is permitted only when explicitly disclosed and approved. For example: disclosing the use of AI-generated code in a PR description.
Submissions must certify that the contributor understands and can maintain the code they submit. Submissions must certify that the contributor understands and can maintain the code they submit.
Generated
+473 -467
View File
File diff suppressed because it is too large. Load diff
+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/) - [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/)
- [ ] ![tautulli_logo](logos/tautulli.png) [Tautulli](https://tautulli.com/) - [ ] ![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 ## Try Out the Demo
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository. To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
Simply run the following command to start a demo: Simply run the following command to start a demo:
@@ -134,8 +139,8 @@ Key:
| Symbol | Status | | Symbol | Status |
|--------|-----------| |--------|-----------|
| ✅ | Supported | | ✅ | Supported |
| ❌ | Missing | | ❌ | Missing |
| 🕒 | Planned | | 🕒 | Planned |
| 🚫 | Won't Add | | 🚫 | Won't Add |
@@ -143,66 +148,82 @@ Key:
| TUI | CLI | Feature | | TUI | CLI | Feature |
|-----|-----|----------------------------------------------------------------------------------------------------------------| |-----|-----|----------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, collections, and blocklist | | ✅ | ✅ | View your library, downloads, collections, and blocklist |
| ✅ | ✅ | View details of a specific movie including description, history, downloaded file info, or the credits | | ✅ | ✅ | View details of a specific movie including description, history, downloaded file info, or the credits |
| ✅ | ✅ | View details of any collection and the movies in them | | ✅ | ✅ | View details of any collection and the movies in them |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings | | 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library or collections | | ✅ | ✅ | Search your library or collections |
| ✅ | ✅ | Add movies to your library | | ✅ | ✅ | Add movies to your library |
| ✅ | ✅ | Delete movies, downloads, and indexers | | ✅ | ✅ | Delete movies, downloads, and indexers |
| ✅ | ✅ | Trigger automatic searches for movies | | ✅ | ✅ | Trigger automatic searches for movies |
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections | | ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
| ✅ | ✅ | Manually search for movies | | ✅ | ✅ | Manually search for movies |
| ✅ | ✅ | Edit your movies, collections, and indexers | | ✅ | ✅ | Edit your movies, collections, and indexers |
| ✅ | ✅ | Manage your tags | | ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders | | ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist | | ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates | | ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks | | ✅ | ✅ | Manually trigger scheduled tasks |
### Sonarr ### Sonarr
| TUI | CLI | Feature | | TUI | CLI | Feature |
|-----|-----|--------------------------------------------------------------------------------------------------------------------| |-----|-----|--------------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, episodes | | ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits | | ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings | | 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library | | ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library | | ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files | | ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes | | ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads | | ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes | | ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers | | ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags | | ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders | | ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist | | ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates | | ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks | | ✅ | ✅ | Manually trigger scheduled tasks |
### Lidarr ### Lidarr
| TUI | CLI | Feature | | TUI | CLI | Feature |
|-----|-----|----------------------------------------------------------------------------------------------------------------| |-----|-----|----------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, tracks | | ✅ | ✅ | View your library, downloads, blocklist, tracks |
| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info | | ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings | | 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library | | ✅ | ✅ | Search your library |
| ✅ | ✅ | Add artists to your library | | ✅ | ✅ | Add artists to your library |
| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files | | ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files |
| ✅ | ✅ | Trigger automatic searches for artists or albums | | ✅ | ✅ | Trigger automatic searches for artists or albums |
| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads | | ✅ | ✅ | Trigger refresh and disk scan for artists and downloads |
| ✅ | ✅ | Manually search for full artist discographies or albums | | ✅ | ✅ | Manually search for full artist discographies or albums |
| ✅ | ✅ | Edit your artists and indexers | | ✅ | ✅ | Edit your artists and indexers |
| ✅ | ✅ | Manage your tags | | ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders | | ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist | | ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates | | ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks | | ✅ | ✅ | Manually trigger scheduled tasks |
### Readarr ### Readarr
- [ ] Support for Readarr | TUI | CLI | Feature |
|-----|-----|----------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, editions |
| ✅ | ✅ | View details of a specific author, book, or edition including description, history, downloaded file info |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add authors to your library |
| ✅ | ✅ | Delete authors, books, downloads, indexers, root folders, and book files |
| ✅ | ✅ | Trigger automatic searches for authors or books |
| ✅ | ✅ | Trigger refresh and disk scan for authors and downloads |
| ✅ | ✅ | Manually search for author releases or books |
| ✅ | ✅ | Edit your authors and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
### Whisparr ### Whisparr
@@ -245,7 +266,7 @@ To see all available commands, simply run `managarr --help`:
```shell ```shell
$ managarr --help $ managarr --help
managarr 0.7.0 managarr 0.8.0
Alex Clarke <alex.j.tusa@gmail.com> Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs A TUI and CLI to manage your Servarrs
@@ -443,6 +464,11 @@ lidarr:
custom_headers: # Example of adding custom headers to all requests to the Servarr instance custom_headers: # Example of adding custom headers to all requests to the Servarr instance
traefik-auth-bypass-key: someBypassKey1234567890 traefik-auth-bypass-key: someBypassKey1234567890
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE} SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
readarr:
- name: Chaptarr (Ebooks) # Example of how to use the Readarr support with a Readarr fork like Chaptarr
uri: http://192.168.0.87:8789/readarr/gr/ebook
api_token: someApiToken1234567890
``` ```
### SSL Configuration ### SSL Configuration
@@ -552,6 +578,46 @@ Managarr supports using environment variables on startup so you don't have to al
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` | | `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` | | `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
## Tested Readarr Forks
The following Readarr forks have been explicitly tested with Managarr:
* [Bookshelf](https://github.com/pennydreadful/bookshelf)
* [Readarr-Resurrected](https://github.com/ricetim/readarr-rresurrected)
* [Librarr](https://github.com/Rorqualx/Librarr)
* [Chaptarr](https://github.com/Chaptarr/Chaptarr)
> [!IMPORTANT]
> Chaptarr houses a dedicated Readarr facade middleware that activates on a path prefix, meaning a bare config doesn't
> work. Instead, users need to configure both Audiobooks and Ebooks separately, and based on their desired metadata
> fetcher (i.e. Goodreads and Hardcover IDs). Because of this, Chaptarr-based configurations must use the `uri` key
> rather than the `host` and `port` keys. The below configuration file demonstrates all the supported variations for
> connecting Managarr to Chaptarr.
### Chaptarr Example Configuration
```yaml
readarr:
# Because Chaptarr supports both eBooks and audiobooks, the same instance must be provisioned twice if you're using
# both so Managarr can differentiate between the two. Thus, for Chaptarr, you need an eBook server configured and an
# audiobook server configured like below.
- name: eBooks (Goodreads) # Readarr originally used Goodreads as its metadata provider
uri: http://192.168.0.87:8789/readarr/gr/ebook # Note the `gr` in the path prefix indicating "GoodReads"
api_token: someApiToken1234567890
- name: Chaptarr Audiobooks (Goodreads)
uri: http://192.168.0.105:8789/readarr/gr/audiobook
api_token: someApiToken1234567890
- name: Chaptarr eBooks (Hardcover)
uri: http://192.168.0.105:8789/readarr/hc/ebook # Note the `hc` in the path prefix indicating "HardCover"
api_token: someApiToken1234567890
- name: Chaptarr Audiobooks (Hardcover)
uri: http://192.168.0.105:8789/readarr/hc/audiobook
api_token: someApiToken1234567890
```
## Screenshots ## Screenshots
### Radarr ### Radarr
@@ -574,6 +640,13 @@ Managarr supports using environment variables on startup so you don't have to al
![artist_discography_search](screenshots/lidarr/artist_discography_search.png) ![artist_discography_search](screenshots/lidarr/artist_discography_search.png)
![manual_album_search](screenshots/lidarr/manual_album_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 ### General
![logs](screenshots/radarr/logs.png) ![logs](screenshots/radarr/logs.png)
![indexers](screenshots/radarr/indexers.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::models::{HorizontallyScrollableText, TabRoute};
use crate::network::NetworkEvent; use crate::network::NetworkEvent;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::sonarr_network::SonarrEvent;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
#[test] #[test]
@@ -165,6 +166,36 @@ mod tests {
assert!(app.is_routing); assert!(app.is_routing);
} }
#[tokio::test]
async fn test_named_tab_uses_selected_servarr_route_and_network_events() {
let (network_tx, mut network_rx) = mpsc::channel(500);
let mut app = App::new(
network_tx,
AppConfig {
radarr: Some(vec![ServarrConfig {
name: Some("Movies".to_owned()),
..ServarrConfig::default()
}]),
sonarr: Some(vec![ServarrConfig {
name: Some("Shows".to_owned()),
..ServarrConfig::default()
}]),
..AppConfig::default()
},
CancellationToken::new(),
);
assert!(app.server_tabs.select_tab_by_title("Shows"));
assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into());
app.on_tick().await;
assert_eq!(
network_rx.recv().await.unwrap(),
SonarrEvent::GetQualityProfiles.into()
);
}
#[test] #[test]
fn test_reset_cancellation_token() { fn test_reset_cancellation_token() {
let mut app = App { let mut app = App {
+5 -8
View File
@@ -263,14 +263,11 @@ impl App<'_> {
} }
pub fn get_current_route(&self) -> Route { pub fn get_current_route(&self) -> Route {
*self.navigation_stack.last().unwrap_or( self
&self .navigation_stack
.server_tabs .last()
.tabs .copied()
.first() .unwrap_or_else(|| self.server_tabs.get_active_route())
.expect("At least one server tab must exist")
.route,
)
} }
} }
+44
View File
@@ -9,7 +9,9 @@ mod tests {
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::cli::lidarr::LidarrCommand; use crate::cli::lidarr::LidarrCommand;
use crate::cli::readarr::ReadarrCommand;
use crate::network::lidarr_network::LidarrEvent; use crate::network::lidarr_network::LidarrEvent;
use crate::network::readarr_network::ReadarrEvent;
use crate::{ use crate::{
Cli, Cli,
app::App, app::App,
@@ -24,6 +26,10 @@ mod tests {
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse, BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
RadarrSerdeable, RadarrSerdeable,
}, },
readarr_models::{
BlocklistItem as ReadarrBlocklistItem, BlocklistResponse as ReadarrBlocklistResponse,
ReadarrSerdeable,
},
sonarr_models::{ sonarr_models::{
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse, BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
SonarrSerdeable, SonarrSerdeable,
@@ -68,6 +74,13 @@ mod tests {
assert_ok!(&result); assert_ok!(&result);
} }
#[test]
fn test_readarr_subcommand_delegates_to_readarr() {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "authors"]);
assert_ok!(&result);
}
#[test] #[test]
fn test_completions_requires_argument() { fn test_completions_requires_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions"]); let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
@@ -218,4 +231,35 @@ mod tests {
assert_ok!(&result); assert_ok!(&result);
} }
#[tokio::test]
async fn test_cli_handler_delegates_readarr_commands_to_the_readarr_cli_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::BlocklistResponse(
ReadarrBlocklistResponse {
records: vec![ReadarrBlocklistItem::default()],
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::ClearBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let clear_blocklist_command = ReadarrCommand::ClearBlocklist.into();
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
assert_ok!(&result);
}
} }
+1
View File
@@ -171,6 +171,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrAddCommand> for ReadarrAddCommandH
quality_profile_id, quality_profile_id,
metadata_profile_id, metadata_profile_id,
tags, tags,
images: Vec::new(),
tag_input_string: None, tag_input_string: None,
add_options: AddAuthorOptions { add_options: AddAuthorOptions {
monitor, monitor,
@@ -447,6 +447,7 @@ mod tests {
quality_profile_id: 1, quality_profile_id: 1,
metadata_profile_id: 1, metadata_profile_id: 1,
tags: vec![1, 2], tags: vec![1, 2],
images: Vec::new(),
tag_input_string: None, tag_input_string: None,
add_options: AddAuthorOptions { add_options: AddAuthorOptions {
monitor: MonitorType::All, monitor: MonitorType::All,
+244 -3
View File
@@ -1,5 +1,20 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::cli::{
Command,
readarr::{ReadarrCommand, list_command_handler::ReadarrListCommand},
};
use pretty_assertions::assert_eq;
#[test]
fn test_readarr_command_from() {
let command = ReadarrCommand::List(ReadarrListCommand::Authors);
let result = Command::from(command.clone());
assert_eq!(result, Command::Readarr(command));
}
mod cli { mod cli {
use clap::CommandFactory; use clap::CommandFactory;
use clap::error::ErrorKind; use clap::error::ErrorKind;
@@ -243,20 +258,246 @@ mod tests {
use serde_json::json; use serde_json::json;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::cli::readarr::add_command_handler::ReadarrAddCommand;
use crate::cli::readarr::edit_command_handler::ReadarrEditCommand;
use crate::cli::readarr::get_command_handler::ReadarrGetCommand;
use crate::cli::readarr::refresh_command_handler::ReadarrRefreshCommand;
use crate::cli::readarr::trigger_automatic_search_command_handler::ReadarrTriggerAutomaticSearchCommand;
use crate::models::readarr_models::{ use crate::models::readarr_models::{
BlocklistItem, BlocklistResponse, ReadarrSerdeable, ReadarrTaskName, Author, BlocklistItem, BlocklistResponse, DeleteParams, ReadarrSerdeable, ReadarrTaskName,
}; };
use crate::models::servarr_models::ReleaseDownloadBody; use crate::models::servarr_models::{IndexerSettings, ReleaseDownloadBody};
use crate::{ use crate::{
app::App, app::App,
cli::{ cli::{
CliCommandHandler, CliCommandHandler,
readarr::{ReadarrCliHandler, ReadarrCommand}, readarr::{
ReadarrCliHandler, ReadarrCommand, delete_command_handler::ReadarrDeleteCommand,
list_command_handler::ReadarrListCommand,
},
}, },
models::Serdeable, models::Serdeable,
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent}, network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
}; };
#[tokio::test]
async fn test_readarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = ReadarrCommand::Add(ReadarrAddCommand::Tag {
name: expected_tag_name,
});
let result = ReadarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = ReadarrCommand::Get(ReadarrGetCommand::SystemStatus);
let result = ReadarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
let expected_delete_author_params = DeleteParams {
id: 7,
delete_files: true,
add_import_list_exclusion: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::DeleteAuthor(expected_delete_author_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_author_command = ReadarrCommand::Delete(ReadarrDeleteCommand::Author {
author_id: 7,
delete_files_from_disk: true,
add_list_exclusion: false,
});
let result = ReadarrCliHandler::with(&app_arc, delete_author_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 26500,
minimum_age: 17,
retention: 43,
rss_sync_interval: 35,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::IndexerSettings(
IndexerSettings {
id: 9,
maximum_size: 31200,
minimum_age: 22,
retention: 58,
rss_sync_interval: 90,
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command =
ReadarrCommand::Edit(ReadarrEditCommand::AllIndexerSettings {
maximum_size: Some(26500),
minimum_age: Some(17),
retention: Some(43),
rss_sync_interval: Some(35),
});
let result = ReadarrCliHandler::with(
&app_arc,
edit_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::ListAuthors.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Authors(vec![
Author::default(),
])))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_authors_command = ReadarrCommand::List(ReadarrListCommand::Authors);
let result = ReadarrCliHandler::with(&app_arc, list_authors_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateAllAuthors.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_all_authors_command = ReadarrCommand::Refresh(ReadarrRefreshCommand::AllAuthors);
let result =
ReadarrCliHandler::with(&app_arc, refresh_all_authors_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_readarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{
let expected_author_id = 7;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
ReadarrEvent::TriggerAutomaticAuthorSearch(expected_author_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command =
ReadarrCommand::TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand::Author {
author_id: expected_author_id,
});
let result = ReadarrCliHandler::with(
&app_arc,
trigger_automatic_search_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test] #[tokio::test]
async fn test_mark_history_item_as_failed_command() { async fn test_mark_history_item_as_failed_command() {
let expected_history_item_id = 1234i64; let expected_history_item_id = 1234i64;
+1 -1
View File
@@ -24,7 +24,7 @@ mod tests {
#[case(Key::Char('q'), "q")] #[case(Key::Char('q'), "q")]
#[case(Key::Ctrl('q'), "ctrl-q")] #[case(Key::Ctrl('q'), "ctrl-q")]
fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) { fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) {
assert_str_eq!(format!("{key}"), format!("{expected_str}")); assert_str_eq!(format!("{key}"), expected_str.to_string());
} }
#[test] #[test]
@@ -768,7 +768,7 @@ mod tests {
mod test_handle_submit { mod test_handle_submit {
use crate::assert_navigation_popped; use crate::assert_navigation_popped;
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::{ use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::{
add_movie_body, add_movie_search_result, collection_movie, add_movie_search_result, collection_movie,
}; };
use crate::models::BlockSelectionState; use crate::models::BlockSelectionState;
use crate::models::radarr_models::Movie; use crate::models::radarr_models::Movie;
@@ -1336,7 +1336,7 @@ mod tests {
use crate::{ use crate::{
assert_navigation_popped, assert_navigation_popped,
handlers::radarr_handlers::radarr_handler_test_utils::utils::{ handlers::radarr_handlers::radarr_handler_test_utils::utils::{
add_movie_body, add_movie_search_result, collection_movie, add_movie_search_result, collection_movie,
}, },
models::{ models::{
BlockSelectionState, BlockSelectionState,
@@ -88,6 +88,7 @@ impl AddAuthorHandler<'_, '_> {
quality_profile_id, quality_profile_id,
metadata_profile_id, metadata_profile_id,
tags: Vec::new(), tags: Vec::new(),
images: Vec::new(),
tag_input_string: Some(tags), tag_input_string: Some(tags),
add_options: AddAuthorOptions { add_options: AddAuthorOptions {
monitor, monitor,
@@ -1237,6 +1237,7 @@ mod tests {
quality_profile_id: 2222, quality_profile_id: 2222,
metadata_profile_id: 4444, metadata_profile_id: 4444,
tags: Vec::default(), tags: Vec::default(),
images: Vec::new(),
tag_input_string: Some("usenet, testing".to_owned()), tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddAuthorOptions { add_options: AddAuthorOptions {
monitor: MonitorType::Future, monitor: MonitorType::Future,
@@ -1628,6 +1629,7 @@ mod tests {
quality_profile_id: 2222, quality_profile_id: 2222,
metadata_profile_id: 4444, metadata_profile_id: 4444,
tags: Vec::default(), tags: Vec::default(),
images: Vec::new(),
tag_input_string: Some("usenet, testing".to_owned()), tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddAuthorOptions { add_options: AddAuthorOptions {
monitor: MonitorType::Future, monitor: MonitorType::Future,
@@ -1795,6 +1797,7 @@ mod tests {
quality_profile_id: 2222, quality_profile_id: 2222,
metadata_profile_id: 4444, metadata_profile_id: 4444,
tags: Vec::default(), tags: Vec::default(),
images: Vec::new(),
tag_input_string: Some("usenet, testing".to_owned()), tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddAuthorOptions { add_options: AddAuthorOptions {
monitor: MonitorType::Future, monitor: MonitorType::Future,
+29 -2
View File
@@ -171,13 +171,40 @@ async fn main() -> Result<()> {
cancellation_token.clone(), cancellation_token.clone(),
))); )));
if args.command.is_none()
&& let Some(name) = args.global.servarr_name.as_deref()
{
let mut app = app.lock().await;
anyhow::ensure!(
app.server_tabs.select_tab_by_title(name.trim()),
"A Servarr titled '{}' was not found in your configuration file",
name.trim()
);
}
match args.command { match args.command {
Some(command) => match command { Some(command) => match command {
Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) | Command::Readarr(_) => { Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) | Command::Readarr(_) => {
if spinner_disabled { if spinner_disabled {
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await; start_cli_no_spinner(
config,
reqwest_client,
cancellation_token,
app,
command,
args.global.servarr_name.as_deref(),
)
.await;
} else { } else {
start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await; start_cli_with_spinner(
config,
reqwest_client,
cancellation_token,
app,
command,
args.global.servarr_name.as_deref(),
)
.await;
} }
} }
Command::Completions { shell } => { Command::Completions { shell } => {
+26
View File
@@ -222,6 +222,8 @@ pub struct AddAuthorBody {
pub quality_profile_id: i64, pub quality_profile_id: i64,
pub metadata_profile_id: i64, pub metadata_profile_id: i64,
pub tags: Vec<i64>, pub tags: Vec<i64>,
// Readarr-Resurrected persists the add body directly and rejects it when the images list is absent
pub images: Vec<Value>,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub tag_input_string: Option<String>, pub tag_input_string: Option<String>,
pub add_options: AddAuthorOptions, pub add_options: AddAuthorOptions,
@@ -280,6 +282,7 @@ pub struct Book {
pub ratings: Option<Ratings>, pub ratings: Option<Ratings>,
pub release_date: Option<DateTime<Utc>>, pub release_date: Option<DateTime<Utc>>,
pub statistics: Option<BookStatistics>, pub statistics: Option<BookStatistics>,
#[serde(default)]
pub grabbed: bool, pub grabbed: bool,
} }
@@ -454,6 +457,29 @@ pub struct ReadarrRelease {
pub quality: QualityWrapper, pub quality: QualityWrapper,
} }
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum ReadarrReleaseResponse {
Releases(Vec<ReadarrRelease>),
// Chaptarr wraps the release list in a filter summary envelope, even behind its Readarr facade
Envelope { releases: Vec<ReadarrRelease> },
}
impl Default for ReadarrReleaseResponse {
fn default() -> Self {
ReadarrReleaseResponse::Releases(Vec::new())
}
}
impl ReadarrReleaseResponse {
pub fn into_releases(self) -> Vec<ReadarrRelease> {
match self {
ReadarrReleaseResponse::Releases(releases)
| ReadarrReleaseResponse::Envelope { releases } => releases,
}
}
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BlocklistItem { pub struct BlocklistItem {
+40 -1
View File
@@ -9,7 +9,7 @@ mod tests {
AddAuthorSearchResult, BlocklistItem, BlocklistResponse, Book, BookFile, DownloadRecord, AddAuthorSearchResult, BlocklistItem, BlocklistResponse, Book, BookFile, DownloadRecord,
DownloadsResponse, Edition, MediaInfo, MonitorType, NewItemMonitorType, DownloadsResponse, Edition, MediaInfo, MonitorType, NewItemMonitorType,
ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrHistoryWrapper, ReadarrRelease, ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrHistoryWrapper, ReadarrRelease,
ReadarrTask, ReadarrTaskName, ReadarrReleaseResponse, ReadarrTask, ReadarrTaskName,
}; };
use crate::models::servarr_models::{ use crate::models::servarr_models::{
DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log,
@@ -497,6 +497,29 @@ mod tests {
assert_eq!(readarr_serdeable, ReadarrSerdeable::Releases(releases)); assert_eq!(readarr_serdeable, ReadarrSerdeable::Releases(releases));
} }
#[test]
fn test_readarr_release_response_default_is_an_empty_release_list() {
assert_eq!(
ReadarrReleaseResponse::default(),
ReadarrReleaseResponse::Releases(Vec::new())
);
}
#[test]
fn test_readarr_release_response_into_releases() {
let releases = vec![ReadarrRelease {
guid: "test".to_owned(),
..ReadarrRelease::default()
}];
let list = ReadarrReleaseResponse::Releases(releases.clone());
let envelope = ReadarrReleaseResponse::Envelope {
releases: releases.clone(),
};
assert_eq!(list.into_releases(), releases);
assert_eq!(envelope.into_releases(), releases);
}
#[test] #[test]
fn test_readarr_serdeable_from_security_config() { fn test_readarr_serdeable_from_security_config() {
let security_config = SecurityConfig { let security_config = SecurityConfig {
@@ -939,6 +962,22 @@ mod tests {
assert_none!(&book.statistics); assert_none!(&book.statistics);
} }
#[test]
fn test_book_deserialization_defaults_grabbed_when_absent() {
let book_json = json!({
"id": 1,
"title": "The Name of the Wind",
"authorId": 3,
"foreignBookId": "test-foreign-book-id",
"monitored": true
});
let book: Book = serde_json::from_value(book_json).unwrap();
assert!(!book.grabbed);
assert!(book.monitored);
}
#[test] #[test]
fn test_edition_deserialization() { fn test_edition_deserialization() {
let edition_json = json!({ let edition_json = json!({
@@ -295,7 +295,7 @@ impl ReadarrData<'_> {
statistics: Some(BookStatistics { statistics: Some(BookStatistics {
book_file_count: 3, book_file_count: 3,
total_book_count: 7, total_book_count: 7,
size_on_disk: 2469606195, size_on_disk: 2457600,
percent_of_books: 42.86, percent_of_books: 42.86,
}), }),
..Book::default() ..Book::default()
@@ -538,7 +538,7 @@ impl ReadarrData<'_> {
author_id: 1, author_id: 1,
book_id: 1, book_id: 1,
path: "/nfs/books/Test Author/Test Book.epub".to_owned(), path: "/nfs/books/Test Author/Test Book.epub".to_owned(),
size: 3543348019, size: 3355443,
date_added: date, date_added: date,
quality: quality.clone(), quality: quality.clone(),
media_info: Some(MediaInfo { media_info: Some(MediaInfo {
@@ -34,7 +34,7 @@ mod tests {
"trackFileCount": 8, "trackFileCount": 8,
"trackCount": 10, "trackCount": 10,
"totalTrackCount": 10, "totalTrackCount": 10,
"sizeOnDisk": 1024, "sizeOnDisk": 317162578,
"percentOfTracks": 99.9 "percentOfTracks": 99.9
} }
}]); }]);
@@ -74,7 +74,7 @@ pub mod test_utils {
"trackFileCount": 8, "trackFileCount": 8,
"trackCount": 10, "trackCount": 10,
"totalTrackCount": 10, "totalTrackCount": 10,
"sizeOnDisk": 1024, "sizeOnDisk": 317162578,
"percentOfTracks": 99.9 "percentOfTracks": 99.9
} }
}"#; }"#;
@@ -250,7 +250,7 @@ pub mod test_utils {
track_file_count: 8, track_file_count: 8,
track_count: 10, track_count: 10,
total_track_count: 10, total_track_count: 10,
size_on_disk: 1024, size_on_disk: 317162578,
percent_of_tracks: 99.9, percent_of_tracks: 99.9,
} }
} }
+4 -4
View File
@@ -11,7 +11,7 @@ use crate::models::stateful_table::StatefulTable;
use crate::models::{Route, ScrollableText}; use crate::models::{Route, ScrollableText};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::{Network, RequestMethod}; use crate::network::{Network, RequestMethod};
use crate::utils::{convert_runtime, convert_to_gb}; use crate::utils::{convert_runtime, format_size};
use anyhow::Result; use anyhow::Result;
use indoc::formatdoc; use indoc::formatdoc;
use log::{debug, info, warn}; use log::{debug, info, warn};
@@ -319,7 +319,7 @@ impl Network<'_, '_> {
.. ..
} = movie_response; } = movie_response;
let (hours, minutes) = convert_runtime(runtime); let (hours, minutes) = convert_runtime(runtime);
let size = convert_to_gb(size_on_disk); let size = format_size(size_on_disk, 2);
let studio = studio.clone().unwrap_or_default(); let studio = studio.clone().unwrap_or_default();
let quality_profile = app let quality_profile = app
.data .data
@@ -374,7 +374,7 @@ impl Network<'_, '_> {
IMDB: {imdb_rating} IMDB: {imdb_rating}
Rotten Tomatoes: {rotten_tomatoes_rating} Rotten Tomatoes: {rotten_tomatoes_rating}
Quality Profile: {quality_profile} Quality Profile: {quality_profile}
Size: {size:.2} GB Size: {size}
Path: {path} Path: {path}
Studio: {studio} Studio: {studio}
Genres: {}", Genres: {}",
@@ -393,7 +393,7 @@ impl Network<'_, '_> {
movie_details_modal.file_details = formatdoc!( movie_details_modal.file_details = formatdoc!(
"Relative Path: {} "Relative Path: {}
Absolute Path: {} Absolute Path: {}
Size: {size:.2} GB Size: {size}
Date Added: {}", Date Added: {}",
file.relative_path, file.relative_path,
file.path, file.path,
@@ -759,7 +759,7 @@ mod tests {
IMDB: IMDB:
Rotten Tomatoes: Rotten Tomatoes:
Quality Profile: HD - 1080p Quality Profile: HD - 1080p
Size: 0.00 GB Size: 0.00 MB
Path: /nfs/movies Path: /nfs/movies
Studio: 21st Century Alex Studio: 21st Century Alex
Genres: cool, family, fun" Genres: cool, family, fun"
@@ -5,7 +5,7 @@ use serde_json::{Value, json};
use crate::models::Route; use crate::models::Route;
use crate::models::readarr_models::{ use crate::models::readarr_models::{
AddAuthorBody, AddAuthorSearchResult, Author, DeleteParams, EditAuthorParams, ReadarrCommandBody, AddAuthorBody, AddAuthorSearchResult, Author, DeleteParams, EditAuthorParams, ReadarrCommandBody,
ReadarrHistoryItem, ReadarrRelease, ReadarrHistoryItem, ReadarrRelease, ReadarrReleaseResponse,
}; };
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
@@ -235,10 +235,15 @@ impl Network<'_, '_> {
.await; .await;
self self
.handle_request::<(), Vec<ReadarrRelease>>(request_props, |release_vec, mut app| { .handle_request::<(), ReadarrReleaseResponse>(request_props, |response, mut app| {
app.data.readarr_data.author_releases.set_items(release_vec); app
.data
.readarr_data
.author_releases
.set_items(response.into_releases());
}) })
.await .await
.map(ReadarrReleaseResponse::into_releases)
} }
pub(in crate::network::readarr_network) async fn get_readarr_author_history( pub(in crate::network::readarr_network) async fn get_readarr_author_history(
@@ -133,6 +133,48 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_handle_get_author_releases_event_unwraps_the_release_envelope() {
let expected_releases = vec![torrent_release()];
let releases_json = json!({
"releases": [serde_json::from_str::<Value>(RELEASE_JSON).unwrap()],
"hiddenReleases": [],
"filterSummary": { "totalResults": 1, "displayedCount": 1 },
"siblingToggleEnabled": false
});
let (mock, app, _server) = MockServarrApi::get()
.returns(releases_json)
.query("authorId=3")
.build_for(ReadarrEvent::GetAuthorReleases(3))
.await;
{
let mut app = app.lock().await;
app.server_tabs.set_index(3);
app
.data
.readarr_data
.author_releases
.set_items(vec![stale_release()]);
}
let mut network = test_network(&app);
let result = network
.handle_readarr_event(ReadarrEvent::GetAuthorReleases(3))
.await;
mock.assert_async().await;
let ReadarrSerdeable::Releases(releases) = result.unwrap() else {
panic!("Expected Releases")
};
assert_eq!(releases, expected_releases);
assert_eq!(
app.lock().await.data.readarr_data.author_releases.items,
expected_releases
);
}
#[tokio::test] #[tokio::test]
async fn test_handle_get_author_releases_event_failure() { async fn test_handle_get_author_releases_event_failure() {
let stale_releases = vec![stale_release()]; let stale_releases = vec![stale_release()];
@@ -638,6 +680,7 @@ mod tests {
"qualityProfileId": 1, "qualityProfileId": 1,
"metadataProfileId": 1, "metadataProfileId": 1,
"tags": [1, 2], "tags": [1, 2],
"images": [],
"addOptions": { "addOptions": {
"monitor": "all", "monitor": "all",
"monitorNewItems": "all", "monitorNewItems": "all",
@@ -680,6 +723,7 @@ mod tests {
"qualityProfileId": 1, "qualityProfileId": 1,
"metadataProfileId": 1, "metadataProfileId": 1,
"tags": [1, 2], "tags": [1, 2],
"images": [],
"addOptions": { "addOptions": {
"monitor": "all", "monitor": "all",
"monitorNewItems": "all", "monitorNewItems": "all",
@@ -5,6 +5,7 @@ use serde_json::{Value, json};
use crate::models::Route; use crate::models::Route;
use crate::models::readarr_models::{ use crate::models::readarr_models::{
Book, BookFile, DeleteParams, Edition, ReadarrCommandBody, ReadarrHistoryItem, ReadarrRelease, Book, BookFile, DeleteParams, Edition, ReadarrCommandBody, ReadarrHistoryItem, ReadarrRelease,
ReadarrReleaseResponse,
}; };
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
use crate::network::readarr_network::ReadarrEvent; use crate::network::readarr_network::ReadarrEvent;
@@ -170,16 +171,19 @@ impl Network<'_, '_> {
.await; .await;
self self
.handle_request::<(), Vec<ReadarrRelease>>(request_props, |release_vec, mut app| { .handle_request::<(), ReadarrReleaseResponse>(request_props, |response, mut app| {
let book_details_modal = app let book_details_modal = app
.data .data
.readarr_data .readarr_data
.book_details_modal .book_details_modal
.get_or_insert_default(); .get_or_insert_default();
book_details_modal.book_releases.set_items(release_vec); book_details_modal
.book_releases
.set_items(response.into_releases());
}) })
.await .await
.map(ReadarrReleaseResponse::into_releases)
} }
pub(in crate::network::readarr_network) async fn get_books( pub(in crate::network::readarr_network) async fn get_books(
@@ -532,6 +532,57 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_handle_get_book_releases_event_unwraps_the_release_envelope() {
let expected_releases = vec![torrent_release()];
let releases_json = json!({
"releases": [serde_json::from_str::<Value>(RELEASE_JSON).unwrap()],
"hiddenReleases": [],
"filterSummary": { "totalResults": 1, "displayedCount": 1 },
"siblingToggleEnabled": false
});
let (mock, app, _server) = MockServarrApi::get()
.returns(releases_json)
.query("bookId=7")
.build_for(ReadarrEvent::GetBookReleases(7))
.await;
{
let mut app = app.lock().await;
app.server_tabs.set_index(3);
let mut book_details_modal = BookDetailsModal::default();
book_details_modal
.book_releases
.set_items(vec![stale_release()]);
app.data.readarr_data.book_details_modal = Some(book_details_modal);
}
let mut network = test_network(&app);
let result = network
.handle_readarr_event(ReadarrEvent::GetBookReleases(7))
.await;
mock.assert_async().await;
let ReadarrSerdeable::Releases(releases) = result.unwrap() else {
panic!("Expected Releases")
};
assert_eq!(releases, expected_releases);
assert_eq!(
app
.lock()
.await
.data
.readarr_data
.book_details_modal
.as_ref()
.unwrap()
.book_releases
.items,
expected_releases
);
}
#[tokio::test] #[tokio::test]
async fn test_handle_get_book_releases_event_empty_book_details_modal() { async fn test_handle_get_book_releases_event_empty_book_details_modal() {
let expected_releases = vec![torrent_release()]; let expected_releases = vec![torrent_release()];
@@ -394,6 +394,7 @@ pub mod test_utils {
quality_profile_id: 1, quality_profile_id: 1,
metadata_profile_id: 1, metadata_profile_id: 1,
tags: Vec::default(), tags: Vec::default(),
images: Vec::new(),
tag_input_string: Some("usenet, testing".to_owned()), tag_input_string: Some("usenet, testing".to_owned()),
add_options: AddAuthorOptions { add_options: AddAuthorOptions {
monitor: MonitorType::All, monitor: MonitorType::All,
@@ -8,7 +8,7 @@ use crate::models::sonarr_models::{
use crate::models::{Route, ScrollableText}; use crate::models::{Route, ScrollableText};
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::network::{Network, RequestMethod}; use crate::network::{Network, RequestMethod};
use crate::utils::convert_to_gb; use crate::utils::format_size;
use anyhow::Result; use anyhow::Result;
use indoc::formatdoc; use indoc::formatdoc;
use log::info; use log::info;
@@ -222,12 +222,12 @@ impl Network<'_, '_> {
overview.unwrap_or_default(), overview.unwrap_or_default(),
)); ));
if let Some(file) = episode_file { if let Some(file) = episode_file {
let size = convert_to_gb(file.size); let size = format_size(file.size, 2);
episode_details_modal.file_details = formatdoc!( episode_details_modal.file_details = formatdoc!(
" "
Relative Path: {} Relative Path: {}
Absolute Path: {} Absolute Path: {}
Size: {size:.2} GB Size: {size}
Language: {} Language: {}
Date Added: {}", Date Added: {}",
file.relative_path, file.relative_path,
+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::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::utils::convert_f64_to_gb; use crate::utils::format_size;
#[cfg(test)] #[cfg(test)]
#[path = "downloads_ui_tests.rs"] #[path = "downloads_ui_tests.rs"]
@@ -97,12 +97,12 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
1f64 - (*sizeleft / *size) 1f64 - (*sizeleft / *size)
}; };
let file_size: f64 = convert_f64_to_gb(*size); let file_size = format_size(*size as i64, 2);
Row::new(vec![ Row::new(vec![
Cell::from(title.to_owned()), Cell::from(title.to_owned()),
Cell::from(format!("{:.0}%", percent * 100.0)), Cell::from(format!("{:.0}%", percent * 100.0)),
Cell::from(format!("{file_size:.2} GB")), Cell::from(file_size),
Cell::from( Cell::from(
output_path output_path
.as_ref() .as_ref()
+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::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs}; use crate::ui::{DrawUi, draw_popup, draw_tabs};
use crate::utils::convert_to_gb; use crate::utils::format_size;
use ratatui::Frame; use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::prelude::{Line, Stylize, Text}; use ratatui::prelude::{Line, Stylize, Text};
@@ -377,7 +377,7 @@ fn draw_album_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
&& active_lidarr_block != ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt, && active_lidarr_block != ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt,
app.should_text_scroll, app.should_text_scroll,
); );
let size = convert_to_gb(*size); let size = format_size(*size, 1);
let rejected_str = if *rejected { "⛔" } else { "" }; let rejected_str = if *rejected { "⛔" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() { let peers = if seeders.is_none() || leechers.is_none() {
Text::from("") Text::from("")
@@ -408,7 +408,7 @@ fn draw_album_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(rejected_str), Cell::from(rejected_str),
Cell::from(title.to_string()), Cell::from(title.to_string()),
Cell::from(indexer.clone()), Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")), Cell::from(size),
Cell::from(peers), Cell::from(peers),
Cell::from(quality_name), Cell::from(quality_name),
]) ])
+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::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs}; use crate::ui::{DrawUi, draw_popup, draw_tabs};
use crate::utils::convert_to_gb; use crate::utils::format_size;
use ratatui::layout::Alignment; use ratatui::layout::Alignment;
use ratatui::text::Text; use ratatui::text::Text;
use serde_json::Number; use serde_json::Number;
@@ -223,7 +223,7 @@ fn draw_artist_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]; ];
if let Some(stats) = current_selection.statistics.as_ref() { if let Some(stats) = current_selection.statistics.as_ref() {
let size = convert_to_gb(stats.size_on_disk); let size = format_size(stats.size_on_disk, 2);
artist_description.extend(vec![ artist_description.extend(vec![
Line::from(vec![ Line::from(vec![
"Albums: ".primary().bold(), "Albums: ".primary().bold(),
@@ -235,7 +235,7 @@ fn draw_artist_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]), ]),
Line::from(vec![ Line::from(vec![
"Size on Disk: ".primary().bold(), "Size on Disk: ".primary().bold(),
format!("{size:.2} GB").default_color(), size.default_color(),
]), ]),
]); ]);
} }
@@ -282,13 +282,10 @@ fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|| "N/A".to_owned(), || "N/A".to_owned(),
|s| format!("{}/{}", s.track_file_count, s.total_track_count), |s| format!("{}/{}", s.track_file_count, s.total_track_count),
); );
let size = album.statistics.as_ref().map_or_else( let size = album
|| "N/A".to_owned(), .statistics
|s| { .as_ref()
let size = convert_to_gb(s.size_on_disk); .map_or_else(|| "N/A".to_owned(), |s| format_size(s.size_on_disk, 2));
format!("{size:.2} GB")
},
);
let duration_mins = album.duration / 60000; let duration_mins = album.duration / 60000;
decorate_album_row_with_style( 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, && active_lidarr_block != ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
app.should_text_scroll, app.should_text_scroll,
); );
let size = convert_to_gb(*size); let size = format_size(*size, 1);
let rejected_str = if *rejected { "⛔" } else { "" }; let rejected_str = if *rejected { "⛔" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() { let peers = if seeders.is_none() || leechers.is_none() {
Text::from("") Text::from("")
@@ -531,7 +528,7 @@ fn draw_artist_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(rejected_str), Cell::from(rejected_str),
Cell::from(title.to_string()), Cell::from(title.to_string()),
Cell::from(indexer.clone()), Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")), Cell::from(size),
Cell::from(peers), Cell::from(peers),
Cell::from(quality_name), Cell::from(quality_name),
]) ])
+3 -3
View File
@@ -13,7 +13,7 @@ use crate::ui::widgets::{
confirmation_prompt::ConfirmationPrompt, confirmation_prompt::ConfirmationPrompt,
popup::{Popup, Size}, popup::{Popup, Size},
}; };
use crate::utils::convert_to_gb; use crate::utils::format_size;
use crate::{ use crate::{
app::App, app::App,
models::{ models::{
@@ -104,7 +104,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let size = artist let size = artist
.statistics .statistics
.as_ref() .as_ref()
.map_or(0f64, |stats| convert_to_gb(stats.size_on_disk)); .map_or(0, |stats| stats.size_on_disk);
let quality_profile = quality_profile_map let quality_profile = quality_profile_map
.get_by_left(&artist.quality_profile_id) .get_by_left(&artist.quality_profile_id)
.cloned() .cloned()
@@ -140,7 +140,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(metadata_profile), Cell::from(metadata_profile),
Cell::from(albums.to_string()), Cell::from(albums.to_string()),
Cell::from(tracks), Cell::from(tracks),
Cell::from(format!("{size:.2} GB")), Cell::from(format_size(size, 2)),
Cell::from(monitored.to_owned()), Cell::from(monitored.to_owned()),
Cell::from(tags), Cell::from(tags),
]), ]),
@@ -10,8 +10,8 @@ expression: output
│ Tracks │ History │ Manual Search │ │ Tracks │ History │ Manual Search │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │ │ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │ │=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless │
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │ │ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless │
│ │ │ │
│ │ │ │
│ │ │ │
@@ -10,8 +10,8 @@ expression: output
│ Tracks │ History │ Manual Search │ │ Tracks │ History │ Manual Search │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │ │ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │ │=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless │
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │ │ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless │
│ │ │ │
│ │ │ │
│ │ │ │
@@ -10,8 +10,8 @@ expression: output
│ Tracks │ History │ Manual Search │ │ Tracks │ History │ Manual Search │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Age ⛔ Title Indexer Size Peers Quality │ │ Source Age ⛔ Title Indexer Size Peers Quality │
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │ │=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless │
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │ │ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless │
│ │ │ │
│ │ │ │
│ │ │ │
@@ -10,8 +10,8 @@ expression: output
│ Tracks │ History │ Manual Search │ │ Tracks │ History │ Manual Search │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Quality │ │ Source ▼ Age ⛔ Title Indexer Size Peers Quality │
│=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │ │=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless │
│ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │ │ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless │
│ │ │ │
│ │ │ │
│ │ │ │
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search ││ ││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││ ││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 302.47 MB ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search │ │ ││ ││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││ ││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││ ││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album │ │ 2023-01-01 302.47 MB ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -24,8 +24,8 @@ expression: output
││ Albums │ History │ Manual Search │ │ ││ ││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││ ││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││ ││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
││=> torrent 1 days ⛔ Test Releas│ │ 0.0 GB 2 / 1 Lossless ││ ││=> torrent 1 days ⛔ Test Releas│ │ 0.0 MB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Releas│ │ 0.0 GB Lossless ││ ││ usenet 1 days ⛔ Test Releas│ │ 0.0 MB Lossless ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -24,8 +24,8 @@ expression: output
││ Albums │ History │ Manual Search │ │ ││ ││ Albums │ History │ Manual Search │ │ ││
││────────────────────────────────────────│Rejection reasons: │───────────────────────────────────────││ ││────────────────────────────────────────│Rejection reasons: │───────────────────────────────────────││
││ Source ▼ Age ⛔ Title │• Unknown quality profile │e Peers Quality ││ ││ Source ▼ Age ⛔ Title │• Unknown quality profile │e Peers Quality ││
││=> torrent 1 days ⛔ Test Relea│• Release is already mapped │ GB 2 / 1 Lossless ││ ││=> torrent 1 days ⛔ Test Relea│• Release is already mapped │ MB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Relea│ │ GB Lossless ││ ││ usenet 1 days ⛔ Test Relea│ │ MB Lossless ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -24,8 +24,8 @@ expression: output
││ Albums │ History │ Manual Search ││ ││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Age ⛔ Title Indexer Size Peers Quality ││ ││ Source Age ⛔ Title Indexer Size Peers Quality ││
││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless ││ ││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless ││ ││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
@@ -24,8 +24,8 @@ expression: output
││ Albums │ History │ Manual Search ││ ││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source ▼ Age ⛔ Title Indexer Size Peers Quality ││ ││ Source ▼ Age ⛔ Title Indexer Size Peers Quality ││
││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless ││ ││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 MB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless ││ ││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 MB Lossless ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search ││ ││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││ ││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 302.47 MB ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search ││ ││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││ ││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 302.47 MB ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search │ │ ││ ││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││ ││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││ ││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album │ │ 2023-01-01 302.47 MB ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -24,8 +24,8 @@ expression: output
││ Albums │ History │ Manual Search │ │ ││ ││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││ ││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││ ││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
││=> torrent 1 days ⛔ Test Releas│ │ 0.0 GB 2 / 1 Lossless ││ ││=> torrent 1 days ⛔ Test Releas│ │ 0.0 MB 2 / 1 Lossless ││
││ usenet 1 days ⛔ Test Releas│ │ 0.0 GB Lossless ││ ││ usenet 1 days ⛔ Test Releas│ │ 0.0 MB Lossless ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search │ │ ││ ││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││ ││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││ ││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album │ │ 2023-01-01 302.47 MB ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search │ │ ││ ││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││ ││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ ╭───╮ │ Release Date Size ││ ││ Monitored Title │ ╭───╮ │ Release Date Size ││
││=> 🏷 Test Album │ Delete Album Files: │ ✔ │ │ 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album │ Delete Album Files: │ ✔ │ │ 2023-01-01 302.47 MB ││
││ │ ╰───╯ │ ││ ││ │ ╰───╯ │ ││
││ │ ╭───╮ │ ││ ││ │ ╭───╮ │ ││
││ │ Add List Exclusion: │ │ │ ││ ││ │ Add List Exclusion: │ │ │ ││
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search │ │ ││ ││ Albums │ History │ Manual Search │ │ ││
││─────────────────────────────────────────│ │───────────────────────────────────────────││ ││─────────────────────────────────────────│ │───────────────────────────────────────────││
││ Monitored Title │ │ Release Date Size ││ ││ Monitored Title │ │ Release Date Size ││
││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album │ │ 2023-01-01 302.47 MB ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │ │Artist: Alex │
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│ │Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││ ││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││
││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││ ││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││
││ Monitored Titl│ ╭─────────────────────────────────────────────────╮ │ize ││ ││ Monitored Titl│ ╭─────────────────────────────────────────────────╮ │ize ││
││=> 🏷 Test│ Quality Profile: │Lossless ▼ │ │.00 GB ││ ││=> 🏷 Test│ Quality Profile: │Lossless ▼ │ │02.47 MB ││
││ │ ╰─────────────────────────────────────────────────╯ │ ││ ││ │ ╰─────────────────────────────────────────────────╯ │ ││
││ │ ╭─────────────────────────────────────────────────╮ │ ││ ││ │ ╭─────────────────────────────────────────────────╮ │ ││
││ │ Metadata Profile: │Standard ▼ │ │ ││ ││ │ Metadata Profile: │Standard ▼ │ │ ││
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │ │Artist: Alex │
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│ │Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │ │Artist: Alex │
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│ │Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Artist: Alex │ │Artist: Alex │
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│ │Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
@@ -24,7 +24,7 @@ expression: output
││ Albums │ History │ Manual Search ││ ││ Albums │ History │ Manual Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Type Tracks Duration Release Date Size ││ ││ Monitored Title Type Tracks Duration Release Date Size ││
││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 0.00 GB ││ ││=> 🏷 Test Album Album 8/10 0 min 2023-01-01 302.47 MB ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
@@ -4,4 +4,4 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
@@ -4,7 +4,7 @@ expression: output
--- ---
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags
=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex => Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex
+3 -3
View File
@@ -37,7 +37,7 @@ use crate::{
servarr_data::lidarr::lidarr_data::LidarrData, servarr_data::lidarr::lidarr_data::LidarrData,
servarr_models::{DiskSpace, RootFolder}, servarr_models::{DiskSpace, RootFolder},
}, },
utils::convert_to_gb, utils::format_size,
}; };
mod blocklist; mod blocklist;
@@ -176,8 +176,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let RootFolder { let RootFolder {
path, free_space, .. path, free_space, ..
} = &monitored_root_folders[i]; } = &monitored_root_folders[i];
let space: f64 = convert_to_gb(*free_space); let space = format_size(*free_space, 2);
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) let root_folder_space = Paragraph::new(format!("{path}: {space} free"))
.block(borderless_block()) .block(borderless_block())
.default_color(); .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::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::utils::convert_to_gb; use crate::utils::format_size;
mod add_root_folder_ui; mod add_root_folder_ui;
@@ -69,11 +69,11 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.. ..
} = root_folders; } = root_folders;
let space: f64 = convert_to_gb(*free_space); let space = format_size(*free_space, 2);
Row::new(vec![ Row::new(vec![
Cell::from(path.to_owned()), Cell::from(path.to_owned()),
Cell::from(format!("{space:.2} GB")), Cell::from(space),
Cell::from( Cell::from(
unmapped_folders unmapped_folders
.as_ref() .as_ref()
@@ -6,7 +6,7 @@ expression: output
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │ │ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │
│=> Alex Person Continuing Lossless Standard 1 12/15 0.00 GB 🏷 alex │ │=> Alex Person Continuing Lossless Standard 1 12/15 0.01 MB 🏷 alex │
│ │ │ │
│ │ │ │
│ │ │ │
@@ -46,7 +46,7 @@ impl DrawUi for CollectionDetailsUi {
draw_popup(f, app, draw_collection_details, Size::Large); draw_popup(f, app, draw_collection_details, Size::Large);
if context_option.unwrap_or(active_radarr_block) == ActiveRadarrBlock::ViewMovieOverview { 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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Collection movie blah blah blah │ │Overview: Collection blah blah blah │
│ │ │Root Folder Path: /nfs/movies │
│It was shot in Madison, Wisconsin: a city the cast never really left. │ │Quality Profile: HD - 1080p │
│ │ │Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
│The sequel took seven years to finish. │ │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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │ │Overview: Collection blah blah blah │
│ │ │Root Folder Path: /nfs/movies │
│ Loading ... │ │Quality Profile: HD - 1080p │
│ │ │Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
│ │ │Monitored: Y│ │ │
│ │ │Search on Ad│ │ │
│ │ │ │ Loading ... │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ Movies ──│ │───────────│
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ Loading ...│ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │ │ │
│ │ │ │
│ │ │ │
@@ -8,40 +8,40 @@ expression: output
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │ │Overview: Collection blah blah blah │
│ │ │Root Folder Path: /nfs/movies │
│ Loading ... │ │Quality Profile: HD - 1080p │
│ │ │Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
│ │ │Monitored: Y│ │ │
│ │ │Search on Ad│ │ │
│ │ │ │ Loading ... │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ Movies ──│ │───────────│
│ │ │ ✔ Title │ │ │
│ │ │=> ✔ Test │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │ │ │
│ │ │ │
│ │ │ │
@@ -8,40 +8,40 @@ expression: output
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│It was shot in Madison, Wisconsin: a city the cast never really left. │ │Overview: Collection blah blah blah │
│ │ │Root Folder Path: /nfs/movies │
│The sequel took seven years to finish. │ │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::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::utils::convert_to_gb; use crate::utils::format_size;
#[cfg(test)] #[cfg(test)]
#[path = "downloads_ui_tests.rs"] #[path = "downloads_ui_tests.rs"]
@@ -96,12 +96,12 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
1f64 - (*sizeleft as f64 / *size as f64) 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![ Row::new(vec![
Cell::from(title.to_owned()), Cell::from(title.to_owned()),
Cell::from(format!("{:.0}%", percent * 100.0)), Cell::from(format!("{:.0}%", percent * 100.0)),
Cell::from(format!("{file_size:.2} GB")), Cell::from(file_size),
Cell::from( Cell::from(
output_path output_path
.as_ref() .as_ref()
+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::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::utils::{convert_runtime, convert_to_gb}; use crate::utils::{convert_runtime, format_size};
mod add_movie_ui; mod add_movie_ui;
mod delete_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 monitored = if movie.monitored { "🏷" } else { "" };
let studio = movie.studio.clone().unwrap_or_default(); let studio = movie.studio.clone().unwrap_or_default();
let (hours, minutes) = convert_runtime(movie.runtime); 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 certification = movie.certification.clone().unwrap_or_default();
let quality_profile = quality_profile_map let quality_profile = quality_profile_map
.get_by_left(&movie.quality_profile_id) .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(format!("{hours}h {minutes}m")),
Cell::from(certification), Cell::from(certification),
Cell::from(movie.original_language.name.to_owned()), 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(quality_profile),
Cell::from(monitored.to_owned()), Cell::from(monitored.to_owned()),
Cell::from(tags), 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::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs}; use crate::ui::{DrawUi, draw_popup, draw_tabs};
use crate::utils::convert_to_gb; use crate::utils::format_size;
#[cfg(test)] #[cfg(test)]
#[path = "movie_details_ui_tests.rs"] #[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(), && current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(),
app.should_text_scroll, app.should_text_scroll,
); );
let size = convert_to_gb(*size); let size = format_size(*size, 1);
let rejected_str = if *rejected { "⛔" } else { "" }; let rejected_str = if *rejected { "⛔" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() { let peers = if seeders.is_none() || leechers.is_none() {
Text::from("") Text::from("")
@@ -434,7 +434,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(rejected_str), Cell::from(rejected_str),
Cell::from(title.to_string()), Cell::from(title.to_string()),
Cell::from(indexer.clone()), Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")), Cell::from(size),
Cell::from(peers), Cell::from(peers),
Cell::from(language), Cell::from(language),
Cell::from(quality), Cell::from(quality),
@@ -12,8 +12,8 @@ expression: output
│ Details │ History │ File │ Cast │ Crew │ Manual Search │ │ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Language Quality │ │ 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 │ │=> 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 GB 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 │ │ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Language Quality │ │ 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 │ │=> 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 GB 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 │ │ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Age ⛔ Title Indexer Size Peers Language Quality │ │ 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 │ │=> 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 GB 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 │ │ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Language Quality │ │ 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 │ │=> 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 GB 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 │ │ Details │ History │ File │ Cast │ Crew │ Manual Search │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source ▼ Age ⛔ Title Indexer Size Peers Language Quality │ │ 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 │ │=> 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 GB 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, line_gauge_with_label, line_gauge_with_title, title_block,
}; };
use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::loading_block::LoadingBlock;
use crate::utils::convert_to_gb; use crate::utils::format_size;
use chrono::Duration; use chrono::Duration;
#[cfg(not(test))] #[cfg(not(test))]
use chrono::Utc; use chrono::Utc;
@@ -170,8 +170,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let RootFolder { let RootFolder {
path, free_space, .. path, free_space, ..
} = &monitored_root_folders[i]; } = &monitored_root_folders[i];
let space: f64 = convert_to_gb(*free_space); let space = format_size(*free_space, 2);
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) let root_folder_space = Paragraph::new(format!("{path}: {space} free"))
.block(borderless_block()) .block(borderless_block())
.default_color(); .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::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_input_box_popup, draw_popup}; use crate::ui::{DrawUi, draw_input_box_popup, draw_popup};
use crate::utils::convert_to_gb; use crate::utils::format_size;
#[cfg(test)] #[cfg(test)]
#[path = "root_folders_ui_tests.rs"] #[path = "root_folders_ui_tests.rs"]
@@ -66,11 +66,11 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.. ..
} = root_folders; } = root_folders;
let space: f64 = convert_to_gb(*free_space); let space = format_size(*free_space, 2);
Row::new(vec![ Row::new(vec![
Cell::from(path.to_owned()), Cell::from(path.to_owned()),
Cell::from(format!("{space:.2} GB")), Cell::from(space),
Cell::from( Cell::from(
unmapped_folders unmapped_folders
.as_ref() .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::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::utils::convert_f64_to_gb; use crate::utils::format_size;
#[cfg(test)] #[cfg(test)]
#[path = "downloads_ui_tests.rs"] #[path = "downloads_ui_tests.rs"]
@@ -97,12 +97,12 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
1f64 - (*sizeleft / *size) 1f64 - (*sizeleft / *size)
}; };
let file_size: f64 = convert_f64_to_gb(*size); let file_size = format_size(*size as i64, 2);
Row::new(vec![ Row::new(vec![
Cell::from(title.to_owned()), Cell::from(title.to_owned()),
Cell::from(format!("{:.0}%", percent * 100.0)), Cell::from(format!("{:.0}%", percent * 100.0)),
Cell::from(format!("{file_size:.2} GB")), Cell::from(file_size),
Cell::from( Cell::from(
output_path output_path
.as_ref() .as_ref()
+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::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs}; use crate::ui::{DrawUi, draw_popup, draw_tabs};
use crate::utils::convert_to_gb; use crate::utils::format_size;
use ratatui::layout::Alignment; use ratatui::layout::Alignment;
use ratatui::text::Text; use ratatui::text::Text;
use serde_json::Number; use serde_json::Number;
@@ -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() { 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![ author_description.extend(vec![
Line::from(vec![ Line::from(vec![
"Books: ".primary().bold(), "Books: ".primary().bold(),
@@ -227,7 +227,7 @@ fn draw_author_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]), ]),
Line::from(vec![ Line::from(vec![
"Size on Disk: ".primary().bold(), "Size on Disk: ".primary().bold(),
format!("{size:.2} GB").default_color(), size.default_color(),
]), ]),
]); ]);
} }
@@ -278,13 +278,10 @@ fn draw_books_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|| "N/A".to_owned(), || "N/A".to_owned(),
|s| format!("{}/{}", s.book_file_count, s.total_book_count), |s| format!("{}/{}", s.book_file_count, s.total_book_count),
); );
let size = book.statistics.as_ref().map_or_else( let size = book
|| "N/A".to_owned(), .statistics
|s| { .as_ref()
let size = convert_to_gb(s.size_on_disk); .map_or_else(|| "N/A".to_owned(), |s| format_size(s.size_on_disk, 2));
format!("{size:.2} GB")
},
);
decorate_book_row_with_style( decorate_book_row_with_style(
book, book,
@@ -483,7 +480,7 @@ fn draw_author_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
&& active_readarr_block != ActiveReadarrBlock::ManualAuthorSearchConfirmPrompt, && active_readarr_block != ActiveReadarrBlock::ManualAuthorSearchConfirmPrompt,
app.should_text_scroll, app.should_text_scroll,
); );
let size = convert_to_gb(*size); let size = format_size(*size, 1);
let rejected_str = if *rejected { "⛔" } else { "" }; let rejected_str = if *rejected { "⛔" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() { let peers = if seeders.is_none() || leechers.is_none() {
Text::from("") Text::from("")
@@ -514,7 +511,7 @@ fn draw_author_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(rejected_str), Cell::from(rejected_str),
Cell::from(title.to_string()), Cell::from(title.to_string()),
Cell::from(indexer.clone()), Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")), Cell::from(size),
Cell::from(peers), Cell::from(peers),
Cell::from(quality_name), Cell::from(quality_name),
]) ])
+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::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs}; use crate::ui::{DrawUi, draw_popup, draw_tabs};
use crate::utils::convert_to_gb; use crate::utils::format_size;
use ratatui::Frame; use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::prelude::{Line, Stylize, Text}; use ratatui::prelude::{Line, Stylize, Text};
@@ -234,7 +234,7 @@ fn draw_book_file_info(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.. ..
} = book_file; } = book_file;
let size = convert_to_gb(*size); let size = format_size(*size, 2);
let audio_info = media_info let audio_info = media_info
.as_ref() .as_ref()
.map(|media_info| { .map(|media_info| {
@@ -249,7 +249,7 @@ fn draw_book_file_info(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Row::new(vec![ Row::new(vec![
Cell::from(path.clone()), Cell::from(path.clone()),
Cell::from(format!("{size:.2} GB")), Cell::from(size),
Cell::from(quality.quality.name.clone()), Cell::from(quality.quality.name.clone()),
Cell::from(audio_info), 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, && active_readarr_block != ActiveReadarrBlock::ManualBookSearchConfirmPrompt,
app.should_text_scroll, app.should_text_scroll,
); );
let size = convert_to_gb(*size); let size = format_size(*size, 1);
let rejected_str = if *rejected { "⛔" } else { "" }; let rejected_str = if *rejected { "⛔" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() { let peers = if seeders.is_none() || leechers.is_none() {
Text::from("") Text::from("")
@@ -426,7 +426,7 @@ fn draw_book_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(rejected_str), Cell::from(rejected_str),
Cell::from(title.to_string()), Cell::from(title.to_string()),
Cell::from(indexer.clone()), Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")), Cell::from(size),
Cell::from(peers), Cell::from(peers),
Cell::from(quality_name), Cell::from(quality_name),
]) ])
+3 -3
View File
@@ -13,7 +13,7 @@ use crate::ui::widgets::{
confirmation_prompt::ConfirmationPrompt, confirmation_prompt::ConfirmationPrompt,
popup::{Popup, Size}, popup::{Popup, Size},
}; };
use crate::utils::convert_to_gb; use crate::utils::format_size;
use crate::{ use crate::{
app::App, app::App,
models::{ models::{
@@ -103,7 +103,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let size = author let size = author
.statistics .statistics
.as_ref() .as_ref()
.map_or(0f64, |stats| convert_to_gb(stats.size_on_disk)); .map_or(0, |stats| stats.size_on_disk);
let quality_profile = quality_profile_map let quality_profile = quality_profile_map
.get_by_left(&author.quality_profile_id) .get_by_left(&author.quality_profile_id)
.cloned() .cloned()
@@ -134,7 +134,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(quality_profile), Cell::from(quality_profile),
Cell::from(metadata_profile), Cell::from(metadata_profile),
Cell::from(books.to_string()), Cell::from(books.to_string()),
Cell::from(format!("{size:.2} GB")), Cell::from(format_size(size, 2)),
Cell::from(monitored.to_owned()), Cell::from(monitored.to_owned()),
Cell::from(tags), Cell::from(tags),
]), ]),
@@ -22,7 +22,7 @@ expression: output
││ Books │ History │ Author Search ││ ││ Books │ History │ Author Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Monitored Title Files Release Date Size ││ ││ 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 ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
@@ -22,7 +22,7 @@ expression: output
││ Books │ History │ Author Search │Do you want to trigger an automatic search of your indexers│ ││ ││ Books │ History │ Author Search │Do you want to trigger an automatic search of your indexers│ ││
││─────────────────────────────────────────│ for all monitored book(s) for the author: Test Author? │───────────────────────────────────────────││ ││─────────────────────────────────────────│ for all monitored book(s) for the author: Test Author? │───────────────────────────────────────────││
││ Monitored Title │ │Date Size ││ ││ Monitored Title │ │Date Size ││
││=> 🏷 Test Book │ │20 2.30 GB ││ ││=> 🏷 Test Book │ │20 2.34 MB ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -22,8 +22,8 @@ expression: output
││ Books │ History │ Author Search │Do you want to trigger an automatic search of your indexers│ ││ ││ Books │ History │ Author Search │Do you want to trigger an automatic search of your indexers│ ││
││─────────────────────────────────────────│ for all monitored book(s) for the author: Test Author? │───────────────────────────────────────────││ ││─────────────────────────────────────────│ for all monitored book(s) for the author: Test Author? │───────────────────────────────────────────││
││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││ ││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││
││=> torrent 1 days ⛔ Test Torren│ │ 0.0 GB 2 / 1 EPUB ││ ││=> torrent 1 days ⛔ Test Torren│ │ 0.0 MB 2 / 1 EPUB ││
││ usenet 1 days ⛔ Test Usenet│ │ 0.0 GB EPUB ││ ││ usenet 1 days ⛔ Test Usenet│ │ 0.0 MB EPUB ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -22,8 +22,8 @@ expression: output
││ Books │ History │ Author Search │ Torrent Release? │ ││ ││ Books │ History │ Author Search │ Torrent Release? │ ││
││────────────────────────────────────────│ │───────────────────────────────────────││ ││────────────────────────────────────────│ │───────────────────────────────────────││
││ Source ▼ Age ⛔ Title │ │e Peers Quality ││ ││ Source ▼ Age ⛔ Title │ │e Peers Quality ││
││=> torrent 1 days ⛔ Test Torre│Rejection reasons: │ GB 2 / 1 EPUB ││ ││=> torrent 1 days ⛔ Test Torre│Rejection reasons: │ MB 2 / 1 EPUB ││
││ usenet 1 days ⛔ Test Usene│• Unknown quality profile │ GB EPUB ││ ││ usenet 1 days ⛔ Test Usene│• Unknown quality profile │ MB EPUB ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
││ │ │ ││ ││ │ │ ││
@@ -22,8 +22,8 @@ expression: output
││ Books │ History │ Author Search ││ ││ Books │ History │ Author Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source Age ⛔ Title Indexer Size Peers Quality ││ ││ Source Age ⛔ Title Indexer Size Peers Quality ││
││=> torrent 1 days ⛔ Test Torrent Release kickass torrents 0.0 GB 2 / 1 EPUB ││ ││=> torrent 1 days ⛔ Test Torrent Release kickass torrents 0.0 MB 2 / 1 EPUB ││
││ usenet 1 days ⛔ Test Usenet Release DrunkenSlug 0.0 GB EPUB ││ ││ usenet 1 days ⛔ Test Usenet Release DrunkenSlug 0.0 MB EPUB ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
@@ -22,8 +22,8 @@ expression: output
││ Books │ History │ Author Search ││ ││ Books │ History │ Author Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
││ Source ▼ Age ⛔ Title Indexer Size Peers Quality ││ ││ Source ▼ Age ⛔ Title Indexer Size Peers Quality ││
││=> torrent 1 days ⛔ Test Torrent Release kickass torrents 0.0 GB 2 / 1 EPUB ││ ││=> torrent 1 days ⛔ Test Torrent Release kickass torrents 0.0 MB 2 / 1 EPUB ││
││ usenet 1 days ⛔ Test Usenet Release DrunkenSlug 0.0 GB EPUB ││ ││ usenet 1 days ⛔ Test Usenet Release DrunkenSlug 0.0 MB EPUB ││
││ ││ ││ ││
││ ││ ││ ││
││ ││ ││ ││
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff. Show more