Compare commits

..

26 Commits

Author SHA1 Message Date
ba1cf0182b build: Updated the docker image so that it ships with trusted CA root certs from trusted providers like LetsEncrypt, DigiCert, etc. for docker SSL users 2026-04-02 09:58:30 -06:00
5cccec88c9 docs: Updated the README to have a more detailed section on how to acquire SSL certificate information 2026-03-30 10:13:51 -06:00
bbcd3f00a9 feat: Created a separate 'ssl' property for the config so users don't have to specify an ssl_cert_path to use SSL or use the uri workaround for HTTPS API access 2026-03-29 12:39:26 -06:00
2e339dd73b docs: Created an authorship policy and PR template that requires explicit acknowledgement of AI assistance 2026-02-24 17:41:34 -07:00
f988cf0f26 docs: Fixed some typos found in the README
Check / stable / fmt (push) Successful in 9m57s
Check / beta / clippy (push) Successful in 11m0s
Check / stable / clippy (push) Successful in 10m59s
Check / nightly / doc (push) Successful in 1m2s
Check / 1.89.0 / check (push) Successful in 1m9s
Test Suite / ubuntu / beta (push) Successful in 1m47s
Test Suite / ubuntu / stable (push) Successful in 1m43s
Test Suite / ubuntu / stable / coverage (push) Successful in 12m52s
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
2026-02-05 18:50:42 -07:00
ff82dc2012 style: Upgraded rustfmt edition to 2024
Check / stable / fmt (push) Successful in 9m57s
Check / beta / clippy (push) Successful in 11m0s
Check / stable / clippy (push) Successful in 10m59s
Check / nightly / doc (push) Successful in 57s
Check / 1.89.0 / check (push) Successful in 1m1s
Test Suite / ubuntu / beta (push) Successful in 1m43s
Test Suite / ubuntu / stable (push) Successful in 1m42s
Test Suite / ubuntu / stable / coverage (push) Successful in 12m52s
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
2026-02-05 10:47:35 -07:00
github-actions[bot]
89a692ad90 chore: bump Cargo.toml to 0.7.1
Check / stable / fmt (push) Successful in 9m55s
Check / beta / clippy (push) Successful in 10m59s
Check / stable / clippy (push) Successful in 10m59s
Check / nightly / doc (push) Successful in 59s
Check / 1.89.0 / check (push) Successful in 1m2s
Test Suite / ubuntu / beta (push) Successful in 1m42s
Test Suite / ubuntu / stable (push) Successful in 1m42s
Test Suite / ubuntu / stable / coverage (push) Successful in 12m51s
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
2026-02-04 18:01:02 +00:00
github-actions[bot]
d77ec5fb34 bump: version 0.7.0 → 0.7.1 [skip ci] 2026-02-04 18:01:00 +00:00
Alex Clarke
ec90e2dca7 Merge pull request #56 from Dark-Alex-17/develop
Check / stable / fmt (push) Successful in 9m53s
Check / beta / clippy (push) Has been cancelled
Check / stable / clippy (push) Has been cancelled
Check / nightly / doc (push) Has been cancelled
Check / 1.89.0 / check (push) Has been cancelled
Test Suite / ubuntu / beta (push) Has been cancelled
Test Suite / ubuntu / stable (push) Has been cancelled
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
Test Suite / ubuntu / stable / coverage (push) Has been cancelled
Hotfix and feature adds for some new issues
2026-02-04 10:38:58 -07:00
5a4e6c9623 style: Addressed CR comments about adding an explicit Command::ConfigPath to the main command match statement 2026-02-04 10:24:59 -07:00
9a6a06ee20 build: Updated all dependencies to resolve dependabot security issues 2026-02-04 09:51:57 -07:00
5556e48fc0 fix: Improved the system notification feature so it can persist between modals 2026-02-04 08:18:04 -07:00
af573cac2a feat: Added support for a system-wide notification popup mechanism that works across Servarrs 2026-02-03 17:03:12 -07:00
447cf6a2b4 style: Applied formatting to the artist_details_ui 2026-02-03 08:10:02 -07:00
203bf9cb66 fix: Sonarr API updated to somtimes allow either seeders or leechers to be null 2026-02-03 08:00:31 -07:00
4f9bc34d23 docs: Updated the README so that the example configuration only includes references to Servarrs that are actually supported [#55] 2026-01-30 15:45:22 -07:00
a2aa9507a9 fix: Improved the first-time run behavior so that it outputs the default configuration file it tries to load to help users locate the file on first-runs 2026-01-30 15:36:26 -07:00
c791b985f0 docs: Updated README to tell users to use 'managarr config-path' as the default method to discover the location of the default configuration file 2026-01-29 12:56:59 -07:00
5c517a748c fix: 'managarr config-path' should work without a pre-existing config already in place [#54] 2026-01-29 12:54:45 -07:00
892c687077 docs: Updated README with config-path as another way to find the default config file for a given system 2026-01-29 10:26:12 -07:00
c6d5b98e86 feat: Implemented a 'config-path' command to print out the default Managarr configuration file path to help address #54 2026-01-29 10:23:05 -07:00
67e5114ec2 build: Removed #[allow(dead_code)] from the LIDARR_LOGO since it is now being utilized 2026-01-26 11:56:05 -07:00
fdc331865e feat: Full support for filtering disks and aggregating root folders in the UI's 'Stats' block 2026-01-26 11:10:59 -07:00
f388dccc08 feat: proper collapsing of root folder paths in the stats layer of the UI 2026-01-22 14:44:48 -07:00
64fad3b9bc refactor: Removed the filtering of monitored_storage_paths from the networking module and migrated all of it to the UI 2026-01-22 13:12:51 -07:00
3be7b09da8 feat: Added config option to filter for specific disk space paths to display in the UI (CLI is unaffected) 2026-01-22 10:49:30 -07:00
48 changed files with 1697 additions and 347 deletions
@@ -0,0 +1,11 @@
### 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
+23
View File
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v0.7.1 (2026-02-04)
### Feat
- Added support for a system-wide notification popup mechanism that works across Servarrs
- Implemented a 'config-path' command to print out the default Managarr configuration file path to help address #54
- Full support for filtering disks and aggregating root folders in the UI's 'Stats' block
- proper collapsing of root folder paths in the stats layer of the UI
- Added config option to filter for specific disk space paths to display in the UI (CLI is unaffected)
- Improved disk-space UI and CLI that shows the actual path being monitored instead of just a disk number
- Implemented the forgotten lidarr list disk-space command
### Fix
- Improved the system notification feature so it can persist between modals
- Sonarr API updated to somtimes allow either seeders or leechers to be null
- Improved the first-time run behavior so that it outputs the default configuration file it tries to load to help users locate the file on first-runs
- 'managarr config-path' should work without a pre-existing config already in place [#54]
### Refactor
- Removed the filtering of monitored_storage_paths from the networking module and migrated all of it to the UI
## v0.7.0 (2026-01-21) ## v0.7.0 (2026-01-21)
### Feat ### Feat
+7
View File
@@ -91,5 +91,12 @@ Then, you can run workflows locally without having to commit and see if the GitH
act -W .github/workflows/release.yml --input_type bump=minor act -W .github/workflows/release.yml --input_type bump=minor
``` ```
## Authorship Policy
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
Claude, etc.) is not permitted unless explicitly disclosed and approved.
Submissions must certify that the contributor understands and can maintain the code they submit.
## Questions? Reach out to me! ## Questions? Reach out to me!
If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can! If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can!
Generated
+124 -162
View File
@@ -99,9 +99,9 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.8.0" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" checksum = "9ded5f9a03ac8f24d1b8a25101ee812cd32cdc8c50a4c50237de2c4915850e73"
dependencies = [ dependencies = [
"rustversion", "rustversion",
] ]
@@ -278,15 +278,15 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.24.0" version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.0" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]] [[package]]
name = "cargo-husky" name = "cargo-husky"
@@ -305,9 +305,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.53" version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -341,9 +341,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.54" version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -351,9 +351,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.54" version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -373,9 +373,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.49" version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -420,14 +420,15 @@ dependencies = [
[[package]] [[package]]
name = "confy" name = "confy"
version = "0.6.1" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0" checksum = "8807c397789cbe02bbdb1a27ea5f345584132808697b2a3f957c829829ee4814"
dependencies = [ dependencies = [
"directories", "etcetera",
"lazy_static",
"serde", "serde",
"serde_yaml", "serde_yaml",
"thiserror 1.0.69", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -561,6 +562,16 @@ dependencies = [
"darling_macro 0.20.11", "darling_macro 0.20.11",
] ]
[[package]]
name = "darling"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core 0.21.3",
"darling_macro 0.21.3",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.23.0" version = "0.23.0"
@@ -585,6 +596,20 @@ dependencies = [
"syn 2.0.114", "syn 2.0.114",
] ]
[[package]]
name = "darling_core"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.114",
]
[[package]] [[package]]
name = "darling_core" name = "darling_core"
version = "0.23.0" version = "0.23.0"
@@ -609,6 +634,17 @@ dependencies = [
"syn 2.0.114", "syn 2.0.114",
] ]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core 0.21.3",
"quote",
"syn 2.0.114",
]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.23.0" version = "0.23.0"
@@ -671,11 +707,11 @@ dependencies = [
[[package]] [[package]]
name = "derive_setters" name = "derive_setters"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" checksum = "b7e6f6fa1f03c14ae082120b84b3c7fbd7b8588d924cf2d7c3daf9afd49df8b9"
dependencies = [ dependencies = [
"darling 0.20.11", "darling 0.21.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.114",
@@ -715,15 +751,6 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
"dirs-sys",
]
[[package]] [[package]]
name = "dirs-next" name = "dirs-next"
version = "2.0.0" version = "2.0.0"
@@ -734,18 +761,6 @@ dependencies = [
"dirs-sys-next", "dirs-sys-next",
] ]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "dirs-sys-next" name = "dirs-sys-next"
version = "0.1.2" version = "0.1.2"
@@ -852,6 +867,17 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "etcetera"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "euclid" name = "euclid"
version = "0.22.13" version = "0.22.13"
@@ -890,9 +916,9 @@ dependencies = [
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]] [[package]]
name = "finl_unicode" name = "finl_unicode"
@@ -1104,6 +1130,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.4.0" version = "1.4.0"
@@ -1228,14 +1263,13 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.19" version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
@@ -1254,9 +1288,9 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.64" version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
@@ -1418,9 +1452,9 @@ dependencies = [
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.46.1" version = "1.46.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8" checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4"
dependencies = [ dependencies = [
"console", "console",
"once_cell", "once_cell",
@@ -1634,7 +1668,7 @@ dependencies = [
[[package]] [[package]]
name = "managarr" name = "managarr"
version = "0.7.0" version = "0.7.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
@@ -1783,9 +1817,9 @@ dependencies = [
[[package]] [[package]]
name = "mockito" name = "mockito"
version = "1.7.1" version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e0603425789b4a70fcc4ac4f5a46a566c116ee3e2a6b768dc623f7719c611de" checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0"
dependencies = [ dependencies = [
"assert-json-diff", "assert-json-diff",
"bytes", "bytes",
@@ -1869,9 +1903,9 @@ dependencies = [
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
@@ -1997,9 +2031,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "openssl-src" name = "openssl-src"
version = "300.5.4+3.5.4" version = "300.5.5+3.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
dependencies = [ dependencies = [
"cc", "cc",
] ]
@@ -2017,12 +2051,6 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "ordered-float" name = "ordered-float"
version = "2.10.1" version = "2.10.1"
@@ -2185,9 +2213,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
@@ -2261,9 +2289,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.105" version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -2295,9 +2323,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.43" version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -2468,9 +2496,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.2" version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -2480,9 +2508,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.13" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -2491,9 +2519,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.8" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]] [[package]]
name = "relative-path" name = "relative-path"
@@ -2911,15 +2939,15 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
@@ -2929,9 +2957,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.60.2", "windows-sys 0.60.2",
@@ -3062,9 +3090,9 @@ dependencies = [
[[package]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.6.1" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"core-foundation", "core-foundation",
@@ -3225,9 +3253,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.45" version = "0.3.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5"
dependencies = [ dependencies = [
"deranged", "deranged",
"libc", "libc",
@@ -3240,9 +3268,9 @@ dependencies = [
[[package]] [[package]]
name = "time-core" name = "time-core"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]] [[package]]
name = "tinystr" name = "tinystr"
@@ -3548,9 +3576,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.19.0" version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
dependencies = [ dependencies = [
"atomic", "atomic",
"getrandom 0.3.4", "getrandom 0.3.4",
@@ -3975,15 +4003,6 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -4020,21 +4039,6 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -4077,12 +4081,6 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -4095,12 +4093,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -4113,12 +4105,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@@ -4143,12 +4129,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@@ -4161,12 +4141,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@@ -4179,12 +4153,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -4197,12 +4165,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -4267,18 +4229,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.33" version = "0.8.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.33" version = "0.8.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -4347,6 +4309,6 @@ dependencies = [
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.16" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
+35 -35
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "managarr" name = "managarr"
version = "0.7.0" version = "0.7.1"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs" description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"] keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
@@ -20,67 +20,67 @@ members = [
] ]
[dependencies] [dependencies]
anyhow = "1.0.68" anyhow = "1.0.100"
backtrace = "0.3.74" backtrace = "0.3.76"
bimap = { version = "0.6.3", features = ["serde"] } bimap = { version = "0.6.3", features = ["serde"] }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.43", features = ["serde"] }
confy = { version = "0.6.0", default-features = false, features = [ confy = { version = "2.0.0", default-features = false, features = [
"yaml_conf", "yaml_conf",
] } ] }
crossterm = "0.28.1" crossterm = "0.28.1"
derivative = "2.2.0" derivative = "2.2.0"
human-panic = "2.0.2" human-panic = "2.0.6"
indoc = "2.0.0" indoc = "2.0.7"
log = "0.4.17" log = "0.4.29"
log4rs = { version = "1.2.0", features = ["file_appender"] } log4rs = { version = "1.4.0", features = ["file_appender"] }
regex = "1.11.1" regex = "1.12.2"
reqwest = { version = "0.12.9", features = ["json"] } reqwest = { version = "0.12.28", features = ["json"] }
serde_yaml = "0.9.16" serde_yaml = "0.9.34"
serde_json = "1.0.91" serde_json = "1.0.149"
serde = { version = "1.0.214", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] }
strum_macros = "0.26.4" strum_macros = "0.26.4"
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.49.0", features = ["full"] }
tokio-util = "0.7.8" tokio-util = "0.7.18"
ratatui = { version = "0.30.0", features = [ ratatui = { version = "0.30.0", features = [
"all-widgets", "all-widgets",
"unstable-widget-ref", "unstable-widget-ref",
] } ] }
urlencoding = "2.1.2" urlencoding = "2.1.3"
clap = { version = "4.5.20", features = [ clap = { version = "4.5.56", features = [
"derive", "derive",
"cargo", "cargo",
"env", "env",
"wrap_help", "wrap_help",
] } ] }
clap_complete = "4.5.33" clap_complete = "4.5.65"
itertools = "0.14.0" itertools = "0.14.0"
ctrlc = "3.4.5" ctrlc = "3.5.1"
colored = "3.0.0" colored = "3.1.1"
async-trait = "0.1.83" async-trait = "0.1.89"
dirs-next = "2.0.0" dirs-next = "2.0.0"
managarr-tree-widget = "0.25.0" managarr-tree-widget = "0.25.0"
indicatif = "0.17.9" indicatif = "0.17.11"
derive_setters = "0.1.6" derive_setters = "0.1.9"
deunicode = "1.6.0" deunicode = "1.6.2"
openssl = { version = "0.10.70", features = ["vendored"] } openssl = { version = "0.10.75", features = ["vendored"] }
veil = "0.2.0" veil = "0.2.0"
validate_theme_derive = "0.1.0" validate_theme_derive = "0.1.0"
enum_display_style_derive = "0.1.0" enum_display_style_derive = "0.1.0"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.16" assert_cmd = "2.1.2"
mockall = "0.13.0" mockall = "0.13.1"
mockito = "1.0.0" mockito = "1.7.1"
pretty_assertions = "1.3.0" pretty_assertions = "1.4.1"
proptest = "1.6.0" proptest = "1.9.0"
rstest = "0.25.0" rstest = "0.25.0"
serial_test = "3.2.0" serial_test = "3.3.1"
assertables = "9.8.2" assertables = "9.8.4"
insta = "1.41.1" insta = "1.46.1"
[dev-dependencies.cargo-husky] [dev-dependencies.cargo-husky]
version = "1" version = "1.5.0"
default-features = false default-features = false
features = ["user-hooks"] features = ["user-hooks"]
+2
View File
@@ -21,6 +21,8 @@ RUN mv target/release/managarr .
FROM debian:stable-slim FROM debian:stable-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
# Copy the compiled binary from the builder container # Copy the compiled binary from the builder container
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
+107 -47
View File
@@ -55,9 +55,9 @@ Run Managarr as a docker container by mounting your `config.yml` file to `/root/
docker run --rm -it -v /home/aclarke/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr:latest docker run --rm -it -v /home/aclarke/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr:latest
``` ```
You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command. You can also clone this repo and run `just build-docker` to build a docker image locally and run it using the above command.
Please note that you will need to create and popular your configuration file first before starting the container. Otherwise, the container will fail to start. Please note that you will need to create and populate your configuration file first before starting the container. Otherwise, the container will fail to start.
**Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path. **Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path.
@@ -259,6 +259,8 @@ Commands:
lidarr Commands for manging your Lidarr instance lidarr Commands for manging your Lidarr instance
completions Generate shell completions for the Managarr CLI completions Generate shell completions for the Managarr CLI
tail-logs Tail Managarr logs tail-logs Tail Managarr logs
config-path Print the full path to the default configuration file.
This file can be changed to another location using the '--config-file' flag
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
@@ -266,14 +268,23 @@ Options:
-V, --version Print version -V, --version Print version
Global Options: Global Options:
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=] --disable-spinner Disable the spinner (can sometimes make parsing output
--config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=] challenging) [env: MANAGARR_DISABLE_SPINNER=]
--themes-file <THEMES_FILE> The Managarr themes file to use [env: MANAGARR_THEMES_FILE=] --config-file <CONFIG_FILE> The Managarr configuration file to use; defaults to the
--theme <THEME> The name of the Managarr theme to use [env: MANAGARR_THEME=] path shown by 'managarr config-path' [env:
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use. MANAGARR_CONFIG_FILE=]
--themes-file <THEMES_FILE> The Managarr themes file to use [env:
MANAGARR_THEMES_FILE=]
--theme <THEME> The name of the Managarr theme to use [env:
MANAGARR_THEME=]
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify
the name of the instance configuration that you want to
use.
This is useful when you have multiple instances of the same Servarr defined in your config file. This is useful when you have multiple instances of the
By default, if left empty, the first configured Servarr instance listed in the config file will be used. same Servarr defined in your config file.
By default, if left empty, the first configured Servarr
instance listed in the config file will be used.
``` ```
All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run: All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run:
@@ -330,21 +341,11 @@ $ managarr radarr list movies | jq '.[] | select(.title == "Ad Astra") | .id'
Managarr assumes reasonable defaults to connect to each service (i.e. Radarr is on localhost:7878), Managarr assumes reasonable defaults to connect to each service (i.e. Radarr is on localhost:7878),
but all servers will require you to input the API token. but all servers will require you to input the API token.
The configuration file is located somewhere different for each OS. The configuration file is located somewhere different for each OS, so run the following command to print out the default
location of the `managarr` configuration file for your system:
### Linux ```shell
``` managarr config-path
$HOME/.config/managarr/config.yml
```
### Mac
```
$HOME/Library/Application Support/managarr/config.yml
```
### Windows
```
%APPDATA%/Roaming/managarr/config.yml
``` ```
## Specify Which Configuration File to Use ## Specify Which Configuration File to Use
@@ -363,43 +364,102 @@ radarr:
- host: 192.168.0.78 - host: 192.168.0.78
port: 7878 port: 7878
api_token: someApiToken1234567890 api_token: someApiToken1234567890
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL ssl_cert_path: /path/to/radarr.crt # Use the specified SSL certificate to connect to this Servarr
sonarr: # Enables SSL regardless of the value of the 'ssl'
- uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port' # See the SSL Configuration section below for more information
- host: 192.168.0.79
port: 7878
api_token: someApiToken1234567890
ssl: true # Use SSL to connect to this Servarr
# This will assume that you have the SSL certificate installed to your system trust store
# See the SSL Configuration section below for more information
- uri: http://htpc.local/radarr # Example of using the 'uri' key instead of 'host' and 'port'
api_token: someApiToken1234567890 api_token: someApiToken1234567890
sonarr:
- host: 192.168.0.89
port: 8989
api_token_file: /root/.config/sonarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file
- name: Anime Sonarr # An example of a custom name for a secondary Sonarr instance - name: Anime Sonarr # An example of a custom name for a secondary Sonarr instance
host: 192.168.0.89 host: 192.168.1.89
port: 8989 port: 8989
api_token: someApiToken1234567890 api_token: someApiToken1234567890
readarr:
- host: 192.168.0.87
port: 8787
api_token_file: /root/.config/readarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file
lidarr: lidarr:
- host: 192.168.0.86 - host: 192.168.0.86
port: 8686 port: 8686
api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables
whisparr: monitored_storage_paths: # Filter which Root Folders or Disk Storage you want displayed in the UI's 'Stats' block
- host: 192.168.0.69 # Note: Setting these values does not affect what shows up in the 'Root Folders' tab of the UI.
port: 6969 - /nfs # An example disk (i.e. '<servarr> list disk-space' command) you want displayed in the UI under 'Storage:'
- /media # An example root folder you want displayed in the UI
# Root folders collapse up to the super-directory to reduce duplication in the UI. For example:
# if you have root folders '/media/tv', '/media/cartoons' and '/media/reality', and you set this
# monitored path, the UI will show '/media/[tv,cartoons,reality]' under Root Folders
- host: 192.168.1.86
port: 8686
api_token: someApiToken1234567890 api_token: someApiToken1234567890
ssl_cert_path: /path/to/whisparr.crt ssl_cert_path: /path/to/lidarr_1.crt
custom_headers: # Example of adding custom headers to all requests to the Servarr instance 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}
bazarr: ```
- host: 192.168.0.67
port: 6767 ### SSL Configuration
api_token: someApiToken1234567890 If your Servarr is using SSL or self-signed certificates, you may need to specify additional configuration options to connect without issues.
prowlarr:
- host: 192.168.0.96
port: 9696 **If your Servarr's domain CA is installed in the system's trust store:**
api_token: someApiToken1234567890 Then you can simply specify `ssl: true` and Managarr will be able to connect to your Servarr:
tautulli:
- host: 192.168.0.81 ```yaml
port: 8181 radarr:
api_token: someApiToken1234567890 - host: 192.168.0.78
port: 7878
api_token: yourApiTokenHere
ssl: true
```
**If your Servarr's domain CA is not installed:**
You'll either need to specify the path to the certificate via the `ssl_cert_path` property, or you'll need to install the certificate into your system store.
To acquire the cert for your Servarr's domain, you can use the following command:
```shell
openssl s_client -show-certs -connect <your-servarr-domain.com>:<port> </dev/null |\
sed -n -e '/-.BEGIN/,/-.END/ p' > /path/to/your/servarr.pem
```
Now, you can either specify `ssl_cert_path: /path/to/your/servarr.pem`:
Example configuration with a certificate that's not installed to the system trust store:
```yaml
radarr:
- host: 192.168.0.78
port: 7878
api_token: yourApiTokenHere
ssl_cert_path: /path/to/your/certificate.crt
```
Or install the certificate into your system's trust store.
For example, if you're on a Debian-based system and have `ca-certificates` installed (`sudo apt install ca-certificates`):
```shell
sudo mv /path/to/your/servarr.pem /usr/local/share/ca-certificates/servarr.pem
sudo update-ca-certificates
```
Example configuration with a certificate that is installed to the system trust store:
```yaml
radarr:
- host: 192.168.0.78
port: 7878
api_token: yourApiTokenHere
ssl: true
``` ```
### Example Multi-Instance Configuration: ### Example Multi-Instance Configuration:
+2 -2
View File
@@ -85,5 +85,5 @@ build build_type='debug':
# Build the docker image # Build the docker image
[group: 'build'] [group: 'build']
build-docker: build-docker version=VERSION:
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMG_NAME}}:{{VERSION}} . @DOCKER_BUILDKIT=1 docker build --rm -t {{IMG_NAME}}:{{version}} .
+1 -1
View File
@@ -1,5 +1,5 @@
tab_spaces=2 tab_spaces=2
edition = "2021" edition = "2024"
reorder_imports = true reorder_imports = true
imports_granularity = "Crate" imports_granularity = "Crate"
group_imports = "StdExternalCrate" group_imports = "StdExternalCrate"
+126 -1
View File
@@ -447,6 +447,78 @@ mod tests {
assert_none!(config.port); assert_none!(config.port);
} }
#[test]
#[serial]
fn test_deserialize_optional_env_var_bool_is_bool() {
let yaml_data = r#"
host: localhost
api_token: "test123"
ssl: true
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_some_eq_x!(&config.ssl, &true);
}
#[test]
#[serial]
fn test_deserialize_optional_env_var_bool_is_string() {
let yaml_data = r#"
host: localhost
api_token: "test123"
ssl: "true"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_some_eq_x!(&config.ssl, &true);
}
#[test]
#[serial]
fn test_deserialize_optional_env_var_bool_is_present() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL", "true") };
let yaml_data = r#"
host: localhost
api_token: "test123"
ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL}
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_some_eq_x!(&config.ssl, &true);
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL") };
}
#[test]
#[serial]
fn test_deserialize_optional_env_var_bool_defaults_to_false() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY", "test") };
let yaml_data = r#"
host: localhost
api_token: "test123"
ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY}
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_some_eq_x!(&config.ssl, &false);
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY") };
}
#[test]
fn test_deserialize_optional_env_var_bool_empty() {
let yaml_data = r#"
host: localhost
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_none!(config.ssl);
}
#[test] #[test]
#[serial] #[serial]
fn test_deserialize_optional_env_var_header_map_is_present() { fn test_deserialize_optional_env_var_header_map_is_present() {
@@ -507,6 +579,56 @@ mod tests {
assert_none!(config.custom_headers); assert_none!(config.custom_headers);
} }
#[test]
#[serial]
fn test_deserialize_optional_env_var_string_vec_is_present() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION", "/path1") };
let expected_monitored_paths = ["/path1", "/path2"];
let yaml_data = r#"
monitored_storage_paths:
- ${TEST_VAR_DESERIALIZE_STRING_VEC_OPTION}
- /path2
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths);
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION") };
}
#[test]
#[serial]
fn test_deserialize_optional_env_var_string_vec_does_not_overwrite_non_env_value() {
unsafe {
std::env::set_var(
"TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE",
"/path3",
)
};
let expected_monitored_paths = ["/path1", "/path2"];
let yaml_data = r#"
monitored_storage_paths:
- /path1
- /path2
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths);
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE") };
}
#[test]
fn test_deserialize_optional_env_var_string_vec_empty() {
let yaml_data = r#"
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_none!(config.monitored_storage_paths);
}
#[test] #[test]
#[serial] #[serial]
fn test_deserialize_optional_u16_env_var_is_present() { fn test_deserialize_optional_u16_env_var_is_present() {
@@ -620,10 +742,11 @@ mod tests {
let api_token = "thisisatest".to_owned(); let api_token = "thisisatest".to_owned();
let api_token_file = "/root/.config/api_token".to_owned(); let api_token_file = "/root/.config/api_token".to_owned();
let ssl_cert_path = "/some/path".to_owned(); let ssl_cert_path = "/some/path".to_owned();
let monitored_storage = vec!["/path1".to_owned(), "/path2".to_owned()];
let mut custom_headers = HeaderMap::new(); let mut custom_headers = HeaderMap::new();
custom_headers.insert("X-Custom-Header", "value".parse().unwrap()); custom_headers.insert("X-Custom-Header", "value".parse().unwrap());
let expected_str = format!( let expected_str = format!(
"ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}) }}" "ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl: Some(true), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}"
); );
let servarr_config = ServarrConfig { let servarr_config = ServarrConfig {
name: Some(name), name: Some(name),
@@ -634,7 +757,9 @@ mod tests {
api_token: Some(api_token), api_token: Some(api_token),
api_token_file: Some(api_token_file), api_token_file: Some(api_token_file),
ssl_cert_path: Some(ssl_cert_path), ssl_cert_path: Some(ssl_cert_path),
ssl: Some(true),
custom_headers: Some(custom_headers), custom_headers: Some(custom_headers),
monitored_storage_paths: Some(monitored_storage),
}; };
assert_str_eq!(format!("{servarr_config:?}"), expected_str); assert_str_eq!(format!("{servarr_config:?}"), expected_str);
+54 -4
View File
@@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken;
use veil::Redact; use veil::Redact;
use crate::cli::Command; use crate::cli::Command;
use crate::models::servarr_data::Notification;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData}; use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
@@ -38,6 +39,7 @@ pub struct App<'a> {
pub server_tabs: TabState, pub server_tabs: TabState,
pub keymapping_table: Option<StatefulTable<KeybindingItem>>, pub keymapping_table: Option<StatefulTable<KeybindingItem>>,
pub error: HorizontallyScrollableText, pub error: HorizontallyScrollableText,
pub notification: Option<Notification>,
pub tick_until_poll: u64, pub tick_until_poll: u64,
pub ticks_until_scroll: u64, pub ticks_until_scroll: u64,
pub tick_count: u64, pub tick_count: u64,
@@ -254,6 +256,7 @@ impl Default for App<'_> {
cancellation_token: CancellationToken::new(), cancellation_token: CancellationToken::new(),
keymapping_table: None, keymapping_table: None,
error: HorizontallyScrollableText::default(), error: HorizontallyScrollableText::default(),
notification: None,
is_first_render: true, is_first_render: true,
server_tabs: TabState::new(Vec::new()), server_tabs: TabState::new(Vec::new()),
tick_until_poll: 400, tick_until_poll: 400,
@@ -346,11 +349,11 @@ pub struct AppConfig {
} }
impl AppConfig { impl AppConfig {
pub fn validate(&self) { pub fn validate(&self, config_path: &str) {
if self.lidarr.is_none() && self.radarr.is_none() && self.sonarr.is_none() { if self.lidarr.is_none() && self.radarr.is_none() && self.sonarr.is_none() {
log_and_print_error( log_and_print_error(format!(
"No Servarr configuration provided in the specified configuration file".to_owned(), "No Servarrs are configured in the file: {config_path}"
); ));
process::exit(1); process::exit(1);
} }
@@ -428,6 +431,8 @@ pub struct ServarrConfig {
pub api_token: Option<String>, pub api_token: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")] #[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub api_token_file: Option<String>, pub api_token_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var_bool")]
pub ssl: Option<bool>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")] #[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub ssl_cert_path: Option<String>, pub ssl_cert_path: Option<String>,
#[serde( #[serde(
@@ -436,6 +441,8 @@ pub struct ServarrConfig {
serialize_with = "serialize_header_map" serialize_with = "serialize_header_map"
)] )]
pub custom_headers: Option<HeaderMap>, pub custom_headers: Option<HeaderMap>,
#[serde(default, deserialize_with = "deserialize_optional_env_var_string_vec")]
pub monitored_storage_paths: Option<Vec<String>>,
} }
impl ServarrConfig { impl ServarrConfig {
@@ -481,7 +488,9 @@ impl Default for ServarrConfig {
api_token: Some(String::new()), api_token: Some(String::new()),
api_token_file: None, api_token_file: None,
ssl_cert_path: None, ssl_cert_path: None,
ssl: None,
custom_headers: None, custom_headers: None,
monitored_storage_paths: None,
} }
} }
} }
@@ -526,6 +535,29 @@ where
} }
} }
fn deserialize_optional_env_var_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrBool {
Bool(bool),
String(String),
}
match StringOrBool::deserialize(deserializer)? {
StringOrBool::Bool(b) => Ok(Some(b)),
StringOrBool::String(s) => {
let val = interpolate_env_vars(&s)
.to_lowercase()
.parse()
.unwrap_or(false);
Ok(Some(val))
}
}
}
fn deserialize_optional_env_var_header_map<'de, D>( fn deserialize_optional_env_var_header_map<'de, D>(
deserializer: D, deserializer: D,
) -> Result<Option<HeaderMap>, D::Error> ) -> Result<Option<HeaderMap>, D::Error>
@@ -548,6 +580,24 @@ where
} }
} }
fn deserialize_optional_env_var_string_vec<'de, D>(
deserializer: D,
) -> Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<Vec<String>> = Option::deserialize(deserializer)?;
match opt {
Some(vec) => Ok(Some(
vec
.into_iter()
.map(|it| interpolate_env_vars(&it))
.collect(),
)),
None => Ok(None),
}
}
fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error> fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
+1 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use clap::{ArgAction, Subcommand, arg}; use clap::{ArgAction, Subcommand};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::LidarrCommand; use super::LidarrCommand;
+2 -2
View File
@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use clap::{Subcommand, arg}; use clap::Subcommand;
use serde_json::json; use serde_json::json;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -59,7 +59,7 @@ pub enum LidarrListCommand {
Artists, Artists,
#[command(about = "List all items in the Lidarr blocklist")] #[command(about = "List all items in the Lidarr blocklist")]
Blocklist, Blocklist,
#[command(about = "List disk space details for all provisioned root folders in Sonarr")] #[command(about = "List disk space details for all provisioned root folders in Lidarr")]
DiskSpace, DiskSpace,
#[command(about = "List all active downloads in Lidarr")] #[command(about = "List all active downloads in Lidarr")]
Downloads { Downloads {
+1 -1
View File
@@ -2,7 +2,7 @@ use std::sync::Arc;
use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler}; use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
use anyhow::Result; use anyhow::Result;
use clap::{Subcommand, arg}; use clap::Subcommand;
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler}; use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler}; use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler};
use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler}; use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
+8 -1
View File
@@ -1,8 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use clap::{Subcommand, command}; use clap::Subcommand;
use clap_complete::Shell; use clap_complete::Shell;
use indoc::indoc;
use lidarr::{LidarrCliHandler, LidarrCommand}; use lidarr::{LidarrCliHandler, LidarrCommand};
use radarr::{RadarrCliHandler, RadarrCommand}; use radarr::{RadarrCliHandler, RadarrCommand};
use sonarr::{SonarrCliHandler, SonarrCommand}; use sonarr::{SonarrCliHandler, SonarrCommand};
@@ -43,6 +44,12 @@ pub enum Command {
#[arg(long, help = "Disable colored log output")] #[arg(long, help = "Disable colored log output")]
no_color: bool, no_color: bool,
}, },
#[command(about = indoc!{"
Print the full path to the default configuration file.
This file can be changed to another location using the '--config-file' flag
"})]
ConfigPath,
} }
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> { pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
+1 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use clap::{ArgAction, Subcommand, arg, command}; use clap::{ArgAction, Subcommand};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::RadarrCommand; use super::RadarrCommand;
+1 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use clap::{Subcommand, command}; use clap::Subcommand;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
+1 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use clap::{Subcommand, command}; use clap::Subcommand;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
+54 -1
View File
@@ -20,10 +20,10 @@ mod tests {
use crate::handlers::{handle_events, populate_keymapping_table}; use crate::handlers::{handle_events, populate_keymapping_table};
use crate::models::HorizontallyScrollableText; use crate::models::HorizontallyScrollableText;
use crate::models::Route; use crate::models::Route;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::servarr_data::{ActiveKeybindingBlock, Notification};
use crate::models::servarr_models::KeybindingItem; use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
@@ -174,6 +174,26 @@ mod tests {
); );
} }
#[test]
fn test_handle_clear_notification() {
let mut app = App::test_default();
app.notification = Some(Notification::new(
"Test".to_owned(),
"Test".to_owned(),
true,
));
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
assert_none!(app.notification);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::MovieDetails.into()
);
}
#[rstest] #[rstest]
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) { fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -284,6 +304,39 @@ mod tests {
); );
} }
#[test]
fn test_handle_events_esc_clears_notification() {
let mut app = App::test_default();
app.notification = Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
));
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
assert_none!(app.notification);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
}
#[test]
fn test_handle_events_esc_does_not_clear_notification_when_none() {
let mut app = App::test_default();
app
.data
.radarr_data
.movies
.set_items(vec![Movie::default()]);
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
assert_none!(app.notification);
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
}
fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem { fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem {
let (key, alt_key) = if key.alt.is_some() { let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string()) (key.key.to_string(), key.alt.as_ref().unwrap().to_string())
+2
View File
@@ -116,6 +116,8 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
} else { } else {
app.keymapping_table = None; app.keymapping_table = None;
} }
} else if matches_key!(esc, key) && app.notification.is_some() {
app.notification.take();
} else { } else {
match app.get_current_route() { match app.get_current_route() {
_ if app.keymapping_table.is_some() => { _ if app.keymapping_table.is_some() => {
-2
View File
@@ -36,8 +36,6 @@ pub const READARR_LOGO: &str = "⠀⠀⠀⠀⠀⣀⣠⣤⣄⣀⠀⠀⠀⠀⠀
⠀⠀⠈⠳⣬⣙⠻⠿⠟⣋⣥⠞⠁⠀⠀ ⠀⠀⠈⠳⣬⣙⠻⠿⠟⣋⣥⠞⠁⠀⠀
⠀⠀⠀⠀⠀⠉⠙⠛⠋⠉⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠉⠙⠛⠋⠉⠀⠀⠀⠀⠀
"; ";
// Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then
#[allow(dead_code)]
pub const LIDARR_LOGO: &str = "⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ pub const LIDARR_LOGO: &str = "⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀
⠀⢠⣾⠟⠋⠀⠀⢀⣀⠀⠙⠻⣷⡄⠀ ⠀⢠⣾⠟⠋⠀⠀⢀⣀⠀⠙⠻⣷⡄⠀
⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄ ⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄
+26 -6
View File
@@ -2,7 +2,7 @@
#[macro_use] #[macro_use]
extern crate assertables; extern crate assertables;
use anyhow::Result; use anyhow::{Context, Result};
use clap::{ use clap::{
Args, CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version, Args, CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version,
}; };
@@ -86,7 +86,7 @@ struct GlobalOpts {
global = true, global = true,
value_parser, value_parser,
env = "MANAGARR_CONFIG_FILE", env = "MANAGARR_CONFIG_FILE",
help = "The Managarr configuration file to use" help = "The Managarr configuration file to use; defaults to the path shown by 'managarr config-path'"
)] )]
config_file: Option<PathBuf>, config_file: Option<PathBuf>,
#[arg( #[arg(
@@ -127,15 +127,30 @@ async fn main() -> Result<()> {
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));
let r = running.clone(); let r = running.clone();
let args = Cli::parse(); let args = Cli::parse();
let mut config = if let Some(ref config_file) = args.global.config_file { let config_file_path = confy::get_configuration_file_path("managarr", "config")?;
load_config(config_file.to_str().expect("Invalid config file specified"))? let default_config_path = config_file_path.display().to_string();
if matches!(args.command, Some(Command::ConfigPath)) {
println!("{default_config_path}");
return Ok(());
}
let (mut config, config_path) = if let Some(ref config_file) = args.global.config_file {
(
load_config(config_file.to_str().expect("Invalid config file specified"))?,
config_file.display().to_string(),
)
} else { } else {
confy::load("managarr", "config")? (
confy::load("managarr", "config")
.with_context(|| format!("Config file at '{default_config_path}' is invalid"))?,
default_config_path,
)
}; };
let theme_name = config.theme.clone(); let theme_name = config.theme.clone();
let spinner_disabled = args.global.disable_spinner; let spinner_disabled = args.global.disable_spinner;
debug!("Managarr loaded using config: {config:?}"); debug!("Managarr loaded using config: {config:?}");
config.validate(); config.validate(&config_path);
config.post_process_initialization(); config.post_process_initialization();
let reqwest_client = build_network_client(&config); let reqwest_client = build_network_client(&config);
@@ -170,6 +185,11 @@ async fn main() -> Result<()> {
generate(shell, &mut cli, "managarr", &mut io::stdout()) generate(shell, &mut cli, "managarr", &mut io::stdout())
} }
Command::TailLogs { no_color } => tail_logs(no_color).await?, Command::TailLogs { no_color } => tail_logs(no_color).await?,
Command::ConfigPath => {
unreachable!(
"ConfigPath command is handled before this match and should be unreachable here"
);
}
}, },
None => { None => {
let app_nw = Arc::clone(&app); let app_nw = Arc::clone(&app);
+17
View File
@@ -10,6 +10,23 @@ pub(in crate::models::servarr_data) mod data_test_utils;
#[cfg(test)] #[cfg(test)]
mod servarr_data_tests; mod servarr_data_tests;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Notification {
pub title: String,
pub message: String,
pub success: bool,
}
impl Notification {
pub fn new(title: String, message: String, success: bool) -> Self {
Self {
title,
message,
success,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ActiveKeybindingBlock { pub enum ActiveKeybindingBlock {
#[default] #[default]
@@ -1,8 +1,10 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::models::lidarr_models::LidarrReleaseDownloadBody; use crate::models::lidarr_models::LidarrReleaseDownloadBody;
use crate::models::servarr_data::Notification;
use crate::network::lidarr_network::LidarrEvent; use crate::network::lidarr_network::LidarrEvent;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
#[tokio::test] #[tokio::test]
@@ -30,5 +32,51 @@ mod tests {
mock.assert_async().await; mock.assert_async().await;
assert_ok!(result); assert_ok!(result);
assert_eq!(
app.lock().await.notification,
Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
))
);
}
#[tokio::test]
async fn test_handle_download_lidarr_release_event_sets_failure_notification_on_error() {
let params = LidarrReleaseDownloadBody {
guid: "1234".to_owned(),
indexer_id: 2,
};
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"guid": "1234",
"indexerId": 2,
}))
.returns(json!({}))
.status(500)
.build_for(LidarrEvent::DownloadRelease(params.clone()))
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
let result = network
.handle_lidarr_event(LidarrEvent::DownloadRelease(params))
.await;
mock.assert_async().await;
assert_err!(result);
let app = app.lock().await;
assert_is_empty!(app.error.text);
assert_some_eq_x!(
&app.notification,
&Notification::new(
"Download Failed".to_owned(),
"Download request failed. Check the logs for more details.".to_owned(),
false,
)
);
} }
} }
+22 -3
View File
@@ -1,4 +1,5 @@
use crate::models::lidarr_models::LidarrReleaseDownloadBody; use crate::models::lidarr_models::LidarrReleaseDownloadBody;
use crate::models::servarr_data::Notification;
use crate::network::lidarr_network::LidarrEvent; use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod}; use crate::network::{Network, RequestMethod};
use anyhow::Result; use anyhow::Result;
@@ -31,8 +32,26 @@ impl Network<'_, '_> {
) )
.await; .await;
self let result = self
.handle_request::<LidarrReleaseDownloadBody, Value>(request_props, |_, _| ()) .handle_request::<LidarrReleaseDownloadBody, Value>(request_props, |_, mut app| {
.await app.notification = Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
));
})
.await;
if result.is_err() {
let mut app = self.app.lock().await;
std::mem::take(&mut app.error.text);
app.notification = Some(Notification::new(
"Download Failed".to_owned(),
"Download request failed. Check the logs for more details.".to_owned(),
false,
));
}
result
} }
} }
@@ -16,21 +16,34 @@ mod tests {
async fn test_handle_get_diskspace_event() { async fn test_handle_get_diskspace_event() {
let diskspace_json = json!([ let diskspace_json = json!([
{ {
"path": "/path1",
"freeSpace": 1111, "freeSpace": 1111,
"totalSpace": 2222, "totalSpace": 2222,
}, },
{ {
"path": "/path2",
"freeSpace": 3333, "freeSpace": 3333,
"totalSpace": 4444 "totalSpace": 4444
} }
]); ]);
let response: Vec<DiskSpace> = serde_json::from_value(diskspace_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get() let (mock, app, _server) = MockServarrApi::get()
.returns(diskspace_json) .returns(diskspace_json)
.build_for(LidarrEvent::GetDiskSpace) .build_for(LidarrEvent::GetDiskSpace)
.await; .await;
app.lock().await.server_tabs.set_index(2); app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app); let mut network = test_network(&app);
let disk_space_vec = vec![
DiskSpace {
path: Some("/path1".to_owned()),
free_space: 1111,
total_space: 2222,
},
DiskSpace {
path: Some("/path2".to_owned()),
free_space: 3333,
total_space: 4444,
},
];
let result = network.handle_lidarr_event(LidarrEvent::GetDiskSpace).await; let result = network.handle_lidarr_event(LidarrEvent::GetDiskSpace).await;
@@ -40,8 +53,11 @@ mod tests {
panic!("Expected DiskSpaces"); panic!("Expected DiskSpaces");
}; };
assert_eq!(disk_spaces, response); assert_eq!(
assert!(!app.lock().await.data.lidarr_data.disk_space_vec.is_empty()); app.lock().await.data.lidarr_data.disk_space_vec,
disk_space_vec
);
assert_eq!(disk_spaces, disk_space_vec);
} }
#[tokio::test] #[tokio::test]
+2 -1
View File
@@ -229,6 +229,7 @@ impl<'a, 'b> Network<'a, 'b> {
uri, uri,
api_token, api_token,
ssl_cert_path, ssl_cert_path,
ssl,
custom_headers: custom_headers_option, custom_headers: custom_headers_option,
.. ..
} = app } = app
@@ -245,7 +246,7 @@ impl<'a, 'b> Network<'a, 'b> {
let mut uri = if let Some(servarr_uri) = uri { let mut uri = if let Some(servarr_uri) = uri {
format!("{servarr_uri}/api/{api_version}{resource}") format!("{servarr_uri}/api/{api_version}{resource}")
} else { } else {
let protocol = if ssl_cert_path.is_some() { let protocol = if ssl_cert_path.is_some() || ssl.unwrap_or(false) {
"https" "https"
} else { } else {
"http" "http"
+78 -1
View File
@@ -409,7 +409,7 @@ mod tests {
#[tokio::test] #[tokio::test]
#[should_panic(expected = "Servarr config is undefined")] #[should_panic(expected = "Servarr config is undefined")]
#[rstest] #[rstest]
async fn test_request_props_from_requires_radarr_config_to_be_present_for_all_network_events( async fn test_request_props_from_requires_config_to_be_present_for_all_network_events(
#[values(RadarrEvent::HealthCheck, SonarrEvent::HealthCheck)] network_event: impl Into<NetworkEvent> #[values(RadarrEvent::HealthCheck, SonarrEvent::HealthCheck)] network_event: impl Into<NetworkEvent>
+ NetworkResource, + NetworkResource,
) { ) {
@@ -492,6 +492,82 @@ mod tests {
assert!(request_props.custom_headers.is_empty()); assert!(request_props.custom_headers.is_empty());
} }
#[rstest]
#[tokio::test]
async fn test_request_props_from_custom_config_ssl_doesnt_affect_ssl_cert_path(
#[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into<NetworkEvent>
+ NetworkResource,
#[values(Some(true), Some(false), None)] ssl_option: Option<bool>,
) {
let api_token = "testToken1234".to_owned();
let app_arc = Arc::new(Mutex::new(App::test_default()));
let resource = network_event.resource();
let servarr_config = ServarrConfig {
host: Some("192.168.0.123".to_owned()),
port: Some(8080),
api_token: Some(api_token.clone()),
ssl_cert_path: Some("/test/cert.crt".to_owned()),
ssl: ssl_option,
..ServarrConfig::default()
};
{
let mut app = app_arc.lock().await;
app.server_tabs.tabs[0].config = Some(servarr_config.clone());
app.server_tabs.tabs[1].config = Some(servarr_config);
}
let network = test_network(&app_arc);
let request_props = network
.request_props_from(network_event, RequestMethod::Get, None::<()>, None, None)
.await;
assert_str_eq!(
request_props.uri,
format!("https://192.168.0.123:8080/api/v3{resource}")
);
assert_eq!(request_props.method, RequestMethod::Get);
assert_eq!(request_props.body, None);
assert_str_eq!(request_props.api_token, api_token);
assert!(request_props.custom_headers.is_empty());
}
#[rstest]
#[tokio::test]
async fn test_request_props_uses_ssl_property(
#[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into<NetworkEvent>
+ NetworkResource,
) {
let api_token = "testToken1234".to_owned();
let app_arc = Arc::new(Mutex::new(App::test_default()));
let resource = network_event.resource();
let servarr_config = ServarrConfig {
host: Some("192.168.0.123".to_owned()),
port: Some(8080),
api_token: Some(api_token.clone()),
ssl: Some(true),
..ServarrConfig::default()
};
{
let mut app = app_arc.lock().await;
app.server_tabs.tabs[0].config = Some(servarr_config.clone());
app.server_tabs.tabs[1].config = Some(servarr_config);
}
let network = test_network(&app_arc);
let request_props = network
.request_props_from(network_event, RequestMethod::Get, None::<()>, None, None)
.await;
assert_str_eq!(
request_props.uri,
format!("https://192.168.0.123:8080/api/v3{resource}")
);
assert_eq!(request_props.method, RequestMethod::Get);
assert_eq!(request_props.body, None);
assert_str_eq!(request_props.api_token, api_token);
assert!(request_props.custom_headers.is_empty());
}
#[rstest] #[rstest]
#[tokio::test] #[tokio::test]
async fn test_request_props_from_custom_config_custom_headers( async fn test_request_props_from_custom_config_custom_headers(
@@ -862,6 +938,7 @@ pub(in crate::network) mod test_utils {
host, host,
port, port,
api_token: Some("test1234".to_owned()), api_token: Some("test1234".to_owned()),
monitored_storage_paths: Some(vec!["/path1".to_owned()]),
..ServarrConfig::default() ..ServarrConfig::default()
}; };
+22 -3
View File
@@ -3,6 +3,7 @@ use crate::models::radarr_models::{
EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease, EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease,
RadarrReleaseDownloadBody, RadarrReleaseDownloadBody,
}; };
use crate::models::servarr_data::Notification;
use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
@@ -85,9 +86,27 @@ impl Network<'_, '_> {
.request_props_from(event, RequestMethod::Post, Some(params), None, None) .request_props_from(event, RequestMethod::Post, Some(params), None, None)
.await; .await;
self let result = self
.handle_request::<RadarrReleaseDownloadBody, Value>(request_props, |_, _| ()) .handle_request::<RadarrReleaseDownloadBody, Value>(request_props, |_, mut app| {
.await app.notification = Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
));
})
.await;
if result.is_err() {
let mut app = self.app.lock().await;
std::mem::take(&mut app.error.text);
app.notification = Some(Notification::new(
"Download Failed".to_owned(),
"Download request failed. Check the logs for more details.".to_owned(),
false,
));
}
result
} }
pub(in crate::network::radarr_network) async fn edit_movie( pub(in crate::network::radarr_network) async fn edit_movie(
@@ -4,6 +4,7 @@ mod tests {
AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, EditMovieParams, AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, EditMovieParams,
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrReleaseDownloadBody, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrReleaseDownloadBody,
}; };
use crate::models::servarr_data::Notification;
use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
@@ -164,14 +165,58 @@ mod tests {
.await; .await;
let mut network = test_network(&app); let mut network = test_network(&app);
assert!( let result = network
network .handle_radarr_event(RadarrEvent::DownloadRelease(expected_body))
.handle_radarr_event(RadarrEvent::DownloadRelease(expected_body)) .await;
.await
.is_ok()
);
mock.assert_async().await; mock.assert_async().await;
assert_ok!(result);
assert_some_eq_x!(
&app.lock().await.notification,
&Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
)
);
}
#[tokio::test]
async fn test_handle_download_radarr_release_event_sets_failure_notification_on_error() {
let expected_body = RadarrReleaseDownloadBody {
guid: "1234".to_owned(),
indexer_id: 2,
movie_id: 1,
};
let body = json!({
"guid": "1234",
"indexerId": 2,
"movieId": 1
});
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(body)
.returns(json!({}))
.status(500)
.build_for(RadarrEvent::DownloadRelease(expected_body.clone()))
.await;
let mut network = test_network(&app);
let result = network
.handle_radarr_event(RadarrEvent::DownloadRelease(expected_body))
.await;
mock.assert_async().await;
assert_err!(result);
let app = app.lock().await;
assert_is_empty!(app.error.text);
assert_some_eq_x!(
&app.notification,
&Notification::new(
"Download Failed".to_owned(),
"Download request failed. Check the logs for more details.".to_owned(),
false,
)
);
} }
#[tokio::test] #[tokio::test]
+22 -3
View File
@@ -1,3 +1,4 @@
use crate::models::servarr_data::Notification;
use crate::models::sonarr_models::SonarrReleaseDownloadBody; use crate::models::sonarr_models::SonarrReleaseDownloadBody;
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::network::{Network, RequestMethod}; use crate::network::{Network, RequestMethod};
@@ -31,8 +32,26 @@ impl Network<'_, '_> {
) )
.await; .await;
self let result = self
.handle_request::<SonarrReleaseDownloadBody, Value>(request_props, |_, _| ()) .handle_request::<SonarrReleaseDownloadBody, Value>(request_props, |_, mut app| {
.await app.notification = Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
));
})
.await;
if result.is_err() {
let mut app = self.app.lock().await;
std::mem::take(&mut app.error.text);
app.notification = Some(Notification::new(
"Download Failed".to_owned(),
"Download request failed. Check the logs for more details.".to_owned(),
false,
));
}
result
} }
} }
@@ -1,8 +1,10 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::models::servarr_data::Notification;
use crate::models::sonarr_models::SonarrReleaseDownloadBody; use crate::models::sonarr_models::SonarrReleaseDownloadBody;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
#[tokio::test] #[tokio::test]
@@ -33,5 +35,54 @@ mod tests {
mock.assert_async().await; mock.assert_async().await;
assert_ok!(result); assert_ok!(result);
assert_eq!(
app.lock().await.notification,
Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
))
);
}
#[tokio::test]
async fn test_handle_download_sonarr_release_event_sets_failure_notification_on_error() {
let params = SonarrReleaseDownloadBody {
guid: "1234".to_owned(),
indexer_id: 2,
series_id: Some(1),
..SonarrReleaseDownloadBody::default()
};
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"guid": "1234",
"indexerId": 2,
"seriesId": 1,
}))
.returns(json!({}))
.status(500)
.build_for(SonarrEvent::DownloadRelease(params.clone()))
.await;
app.lock().await.server_tabs.next();
let mut network = test_network(&app);
let result = network
.handle_sonarr_event(SonarrEvent::DownloadRelease(params))
.await;
mock.assert_async().await;
assert_err!(result);
let app = app.lock().await;
assert_is_empty!(app.error.text);
assert_some_eq_x!(
&app.notification,
&Notification::new(
"Download Failed".to_owned(),
"Download request failed. Check the logs for more details.".to_owned(),
false,
)
);
} }
} }
+2 -2
View File
@@ -356,12 +356,12 @@ fn draw_album_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.clone() .clone()
.unwrap_or(Number::from(0u64)) .unwrap_or(Number::from(0u64))
.as_u64() .as_u64()
.unwrap(); .unwrap_or_default();
let leechers = leechers let leechers = leechers
.clone() .clone()
.unwrap_or(Number::from(0u64)) .unwrap_or(Number::from(0u64))
.as_u64() .as_u64()
.unwrap(); .unwrap_or_default();
decorate_peer_style( decorate_peer_style(
seeders, seeders,
@@ -501,12 +501,12 @@ fn draw_artist_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.clone() .clone()
.unwrap_or(Number::from(0u64)) .unwrap_or(Number::from(0u64))
.as_u64() .as_u64()
.unwrap(); .unwrap_or_default();
let leechers = leechers let leechers = leechers
.clone() .clone()
.unwrap_or(Number::from(0u64)) .unwrap_or(Number::from(0u64))
.as_u64() .as_u64()
.unwrap(); .unwrap_or_default();
decorate_peer_style( decorate_peer_style(
seeders, seeders,
+11 -9
View File
@@ -1,5 +1,3 @@
use std::{cmp, iter};
#[cfg(test)] #[cfg(test)]
use crate::ui::ui_test_utils::test_utils::Utc; use crate::ui::ui_test_utils::test_utils::Utc;
use chrono::Duration; use chrono::Duration;
@@ -14,6 +12,7 @@ use ratatui::{
text::Text, text::Text,
widgets::Paragraph, widgets::Paragraph,
}; };
use std::{cmp, iter};
use super::{ use super::{
DrawUi, draw_tabs, DrawUi, draw_tabs,
@@ -28,6 +27,7 @@ use crate::ui::lidarr_ui::downloads::DownloadsUi;
use crate::ui::lidarr_ui::indexers::IndexersUi; use crate::ui::lidarr_ui::indexers::IndexersUi;
use crate::ui::lidarr_ui::root_folders::RootFoldersUi; use crate::ui::lidarr_ui::root_folders::RootFoldersUi;
use crate::ui::lidarr_ui::system::SystemUi; use crate::ui::lidarr_ui::system::SystemUi;
use crate::ui::utils::{extract_monitored_disk_space_vec, extract_monitored_root_folders};
use crate::{ use crate::{
app::App, app::App,
logos::LIDARR_LOGO, logos::LIDARR_LOGO,
@@ -100,6 +100,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time, start_time,
.. ..
} = &app.data.lidarr_data; } = &app.data.lidarr_data;
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
let mut constraints = vec![ let mut constraints = vec![
Constraint::Length(1), Constraint::Length(1),
@@ -110,7 +112,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append( constraints.append(
&mut iter::repeat_n( &mut iter::repeat_n(
Constraint::Length(1), Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1, monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
) )
.collect(), .collect(),
); );
@@ -146,12 +148,12 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(uptime_paragraph, stat_item_areas[1]); f.render_widget(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]); f.render_widget(storage, stat_item_areas[2]);
for i in 0..disk_space_vec.len() { for i in 0..monitored_disk_space_vec.len() {
let DiskSpace { let DiskSpace {
path, path,
free_space, free_space,
total_space, total_space,
} = &disk_space_vec[i]; } = &monitored_disk_space_vec[i];
let title = if let Some(path) = path { let title = if let Some(path) = path {
path path
} else { } else {
@@ -168,12 +170,12 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(space_gauge, stat_item_areas[i + 3]); f.render_widget(space_gauge, stat_item_areas[i + 3]);
} }
f.render_widget(folders, stat_item_areas[disk_space_vec.len() + 3]); f.render_widget(folders, stat_item_areas[monitored_disk_space_vec.len() + 3]);
for i in 0..root_folders.items.len() { for i in 0..monitored_root_folders.len() {
let RootFolder { let RootFolder {
path, free_space, .. path, free_space, ..
} = &root_folders.items[i]; } = &monitored_root_folders[i];
let space: f64 = convert_to_gb(*free_space); let space: f64 = convert_to_gb(*free_space);
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free"))
.block(borderless_block()) .block(borderless_block())
@@ -181,7 +183,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget( f.render_widget(
root_folder_space, root_folder_space,
stat_item_areas[i + disk_space_vec.len() + 4], stat_item_areas[i + monitored_disk_space_vec.len() + 4],
) )
} }
} else { } else {
+23 -1
View File
@@ -14,6 +14,7 @@ use sonarr_ui::SonarrUi;
use utils::layout_block; use utils::layout_block;
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::Notification;
use crate::models::servarr_models::KeybindingItem; use crate::models::servarr_models::KeybindingItem;
use crate::models::{HorizontallyScrollableText, Route, TabState}; use crate::models::{HorizontallyScrollableText, Route, TabState};
use crate::ui::radarr_ui::RadarrUi; use crate::ui::radarr_ui::RadarrUi;
@@ -25,7 +26,8 @@ use crate::ui::utils::{
}; };
use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::Size; use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
mod builtin_themes; mod builtin_themes;
mod lidarr_ui; mod lidarr_ui;
@@ -95,6 +97,10 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) {
_ => (), _ => (),
} }
if let Some(notification) = &app.notification {
draw_notification_popup(f, notification);
}
if app.keymapping_table.is_some() { if app.keymapping_table.is_some() {
draw_help_popup(f, app); draw_help_popup(f, app);
} }
@@ -183,6 +189,22 @@ pub fn draw_help_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
f.render_widget(keymapping_table, table_area); f.render_widget(keymapping_table, table_area);
} }
fn draw_notification_popup(f: &mut Frame<'_>, notification: &Notification) {
let style = if notification.success {
styles::success_style().bold()
} else {
styles::failure_style().bold()
};
let popup = Popup::new(
Message::new(notification.message.as_str())
.title(notification.title.as_str())
.style(style),
)
.size(Size::Message);
f.render_widget(popup, f.area());
}
fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect { fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect {
if title.is_empty() { if title.is_empty() {
f.render_widget(layout_block().default_color(), area); f.render_widget(layout_block().default_color(), area);
+23 -20
View File
@@ -1,15 +1,3 @@
#[cfg(test)]
use crate::ui::ui_test_utils::test_utils::Utc;
use chrono::Duration;
#[cfg(not(test))]
use chrono::Utc;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::Stylize;
use ratatui::text::Text;
use ratatui::widgets::{Paragraph, Row};
use std::{cmp, iter};
use crate::app::App; use crate::app::App;
use crate::logos::RADARR_LOGO; use crate::logos::RADARR_LOGO;
use crate::models::Route; use crate::models::Route;
@@ -27,11 +15,23 @@ use crate::ui::radarr_ui::library::LibraryUi;
use crate::ui::radarr_ui::root_folders::RootFoldersUi; use crate::ui::radarr_ui::root_folders::RootFoldersUi;
use crate::ui::radarr_ui::system::SystemUi; use crate::ui::radarr_ui::system::SystemUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
#[cfg(test)]
use crate::ui::ui_test_utils::test_utils::Utc;
use crate::ui::utils::{ use crate::ui::utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block, borderless_block, extract_monitored_disk_space_vec, extract_monitored_root_folders, layout_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::convert_to_gb;
use chrono::Duration;
#[cfg(not(test))]
use chrono::Utc;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::Stylize;
use ratatui::text::Text;
use ratatui::widgets::{Paragraph, Row};
use std::{cmp, iter};
mod blocklist; mod blocklist;
mod collections; mod collections;
@@ -93,6 +93,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time, start_time,
.. ..
} = &app.data.radarr_data; } = &app.data.radarr_data;
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
let mut constraints = vec![ let mut constraints = vec![
Constraint::Length(1), Constraint::Length(1),
@@ -103,7 +105,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append( constraints.append(
&mut iter::repeat_n( &mut iter::repeat_n(
Constraint::Length(1), Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1, monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
) )
.collect(), .collect(),
); );
@@ -139,12 +141,12 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(uptime_paragraph, stat_item_areas[1]); f.render_widget(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]); f.render_widget(storage, stat_item_areas[2]);
for i in 0..disk_space_vec.len() { for i in 0..monitored_disk_space_vec.len() {
let DiskSpace { let DiskSpace {
path, path,
free_space, free_space,
total_space, total_space,
} = &disk_space_vec[i]; } = &monitored_disk_space_vec[i];
let title = if let Some(path) = path { let title = if let Some(path) = path {
path path
} else { } else {
@@ -161,12 +163,13 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(space_gauge, stat_item_areas[i + 3]); f.render_widget(space_gauge, stat_item_areas[i + 3]);
} }
f.render_widget(folders, stat_item_areas[disk_space_vec.len() + 3]); f.render_widget(folders, stat_item_areas[monitored_disk_space_vec.len() + 3]);
for i in 0..root_folders.items.len() { let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
for i in 0..monitored_root_folders.len() {
let RootFolder { let RootFolder {
path, free_space, .. path, free_space, ..
} = &root_folders.items[i]; } = &monitored_root_folders[i];
let space: f64 = convert_to_gb(*free_space); let space: f64 = convert_to_gb(*free_space);
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free"))
.block(borderless_block()) .block(borderless_block())
@@ -174,7 +177,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget( f.render_widget(
root_folder_space, root_folder_space,
stat_item_areas[i + disk_space_vec.len() + 4], stat_item_areas[i + monitored_disk_space_vec.len() + 4],
) )
} }
} else { } else {
@@ -0,0 +1,54 @@
---
source: src/ui/ui_tests.rs
expression: output
---
╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Radarr │ Sonarr │ Lidarr <?> to open help│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮
│Lidarr Version: 1.2.3.4 ││Test download title ││ ⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ │
│Uptime: 0d 00:00:44 ││50% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ⠀⢠⣾⠟⠋⠀⠀⢀⣀⠀⠙⠻⣷⡄⠀ │
│Storage: ││ ││ ⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄ │
│/path: 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ││ ⣾⡟⠀⢸⠃⠀⠀⠀⠈⠉⠉⠁⠀⢹⣷ │
│Root Folders: ││ ││ ⢿⣧⠀⠈⠀⠀⠀⠀⠀⠀⢀⡟⠀⣸⡿ │
│/nfs: 204800.00 GB free ││ ││ ⠘⣿⣄⠀⠻⣿⣿⡇⠀⢀⠞⠀⣠⣿⠃ │
│ ││ ││ ⠀⠘⢿⣦⣄⠀⠉⠁⠀⠀⣠⣴⡿⠃⠀ │
│ ││ ││ ⠀⠀⠀⠉⠻⠿⢿⡆⡾⠿⠟⠉⠀⠀⠀ │
╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │
│=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex │
│ │
│ │
│ │
│ │
│ │
│ ╭────────── Download Result ──────────╮ │
│ │ Download request sent successfully │ │
│ │ │ │
│ ╰───────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,54 @@
---
source: src/ui/ui_tests.rs
expression: output
---
╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Radarr │ Sonarr │ Lidarr <?> to open help│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮
│Radarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │
│Uptime: 0d 00:00:44 ││50% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ⠀⣿⡇⠀⠈⠙⠻⢿⣶⣤⡀⠀⠀⠀⠀ │
│Storage: ││ ││ ⠀⣿⡇⠀⠀⠀⠀⠀⠈⠙⠻⢷⣦⡄⠀ │
│/path: 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ││ ⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢉⠻⠀ │
│Root Folders: ││ ││ ⠀⣿⡇⠀⠀⠀⠀⠀⢀⣠⣴⣾⠿⠀⠀ │
│/nfs: 204800.00 GB free ││ ││ ⠀⢿⡇⠀⠀⣀⣤⣶⡿⠛⠉⠀⠀⠀⠀ │
│ ││ ││ ⠀⠀⠰⠶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀ │
│ ││ ││ │
╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯
╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Title ▼ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags │
│=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p 🏷 alex │
│ │
│ │
│ │
│ │
│ │
│ ╭────────── Download Failed ──────────╮ │
│ │ Request failed. Received 500 response │ │
│ │ code │ │
│ ╰───────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,54 @@
---
source: src/ui/ui_tests.rs
expression: output
---
╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Radarr │ Sonarr │ Lidarr <?> to open help│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮
│Radarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │
│Uptime: 0d 00:00:44 ││50% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ⠀⣿⡇⠀⠈⠙⠻⢿⣶⣤⡀⠀⠀⠀⠀ │
│Storage: ││ ││ ⠀⣿⡇⠀⠀⠀⠀⠀⠈⠙⠻⢷⣦⡄⠀ │
│/path: 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ││ ⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢉⠻⠀ │
│Root Folders: ││ ││ ⠀⣿⡇⠀⠀⠀⠀⠀⢀⣠⣴⣾⠿⠀⠀ │
│/nfs: 204800.00 GB free ││ ││ ⠀⢿⡇⠀⠀⣀⣤⣶⡿⠛⠉⠀⠀⠀⠀ │
│ ││ ││ ⠀⠀⠰⠶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀ │
│ ││ ││ │
╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯
╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Title ▼ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags │
│=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p 🏷 alex │
│ │
│ │
│ │
│ │
│ │
│ ╭────────── Download Result ──────────╮ │
│ │ Download request sent successfully │ │
│ │ │ │
│ ╰───────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,54 @@
---
source: src/ui/ui_tests.rs
expression: output
---
╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Radarr │ Sonarr │ Lidarr <?> to open help│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮
│Sonarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣶⣦⣄⠀⠀⠀ │
│Uptime: 0d 00:00:44 ││50% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ⠀⣠⡀⠈⠻⣿⣿⣿⣿⣿⣿⠟⠁⢀⣀⠀ │
│Storage: ││ ││ ⢰⣿⣿⣦⠐⠄⠉⠉⠉⠉⠠⠂⣰⣿⣿⡆ │
│/path: 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ││ ⣿⣿⣿⣿⡆⠀⣴⣿⣿⣦⠀⢰⣿⣿⣿⣿ │
│Root Folders: ││ ││ ⣿⣿⣿⣿⡇⠀⠻⣿⣿⠟⠀⠸⣿⣿⣿⣿ │
│/nfs: 204800.00 GB free ││ ││ ⠸⣿⣿⠟⠠⠂⠀⢀⡀⠀⠐⠄⠻⣿⣿⠇ │
│ ││ ││ ⠀⠙⠁⢀⣴⣾⣿⣿⣿⣿⣷⣦⡀⠈⠋⠀ │
│ ││ ││ ⠀⠀⠀⠘⠻⠿⣿⣿⣿⣿⠿⠟⠋⠀⠀⠀ │
╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯
╭ Series ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Title ▼ Year Network Status Rating Type Quality Profile Language Size Monitored Tags │
│=> Test 2022 HBO Continuin TV-MA Standard Bluray-1080p English 59.51 GB 🏷 │
│ │
│ │
│ │
│ │
│ │
│ ╭────────── Download Result ──────────╮ │
│ │ Download request sent successfully │ │
│ │ │ │
│ ╰───────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -425,12 +425,12 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.clone() .clone()
.unwrap_or(Number::from(0u64)) .unwrap_or(Number::from(0u64))
.as_u64() .as_u64()
.unwrap(); .unwrap_or_default();
let leechers = leechers let leechers = leechers
.clone() .clone()
.unwrap_or(Number::from(0u64)) .unwrap_or(Number::from(0u64))
.as_u64() .as_u64()
.unwrap(); .unwrap_or_default();
decorate_peer_style( decorate_peer_style(
seeders, seeders,
@@ -388,12 +388,12 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.clone() .clone()
.unwrap_or(Number::from(0u64)) .unwrap_or(Number::from(0u64))
.as_u64() .as_u64()
.unwrap(); .unwrap_or_default();
let leechers = leechers let leechers = leechers
.clone() .clone()
.unwrap_or(Number::from(0u64)) .unwrap_or(Number::from(0u64))
.as_u64() .as_u64()
.unwrap(); .unwrap_or_default();
decorate_peer_style( decorate_peer_style(
seeders, seeders,
+19 -18
View File
@@ -1,5 +1,3 @@
use std::{cmp, iter};
#[cfg(test)] #[cfg(test)]
use crate::ui::ui_test_utils::test_utils::Utc; use crate::ui::ui_test_utils::test_utils::Utc;
use blocklist::BlocklistUi; use blocklist::BlocklistUi;
@@ -18,8 +16,18 @@ use ratatui::{
widgets::Paragraph, widgets::Paragraph,
}; };
use root_folders::RootFoldersUi; use root_folders::RootFoldersUi;
use std::{cmp, iter};
use system::SystemUi; use system::SystemUi;
use super::{
DrawUi, draw_tabs,
styles::ManagarrStyle,
utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
},
widgets::loading_block::LoadingBlock,
};
use crate::ui::utils::{extract_monitored_disk_space_vec, extract_monitored_root_folders};
use crate::{ use crate::{
app::App, app::App,
logos::SONARR_LOGO, logos::SONARR_LOGO,
@@ -32,15 +40,6 @@ use crate::{
utils::convert_to_gb, utils::convert_to_gb,
}; };
use super::{
DrawUi, draw_tabs,
styles::ManagarrStyle,
utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
},
widgets::loading_block::LoadingBlock,
};
mod blocklist; mod blocklist;
mod downloads; mod downloads;
mod history; mod history;
@@ -101,6 +100,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time, start_time,
.. ..
} = &app.data.sonarr_data; } = &app.data.sonarr_data;
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
let mut constraints = vec![ let mut constraints = vec![
Constraint::Length(1), Constraint::Length(1),
@@ -111,7 +112,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append( constraints.append(
&mut iter::repeat_n( &mut iter::repeat_n(
Constraint::Length(1), Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1, monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
) )
.collect(), .collect(),
); );
@@ -147,13 +148,13 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(uptime_paragraph, stat_item_areas[1]); f.render_widget(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]); f.render_widget(storage, stat_item_areas[2]);
for i in 0..disk_space_vec.len() { for i in 0..monitored_disk_space_vec.len() {
let DiskSpace { let DiskSpace {
path, path,
free_space, free_space,
total_space, total_space,
.. ..
} = &disk_space_vec[i]; } = &monitored_disk_space_vec[i];
let title = if let Some(path) = path { let title = if let Some(path) = path {
path path
} else { } else {
@@ -170,12 +171,12 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(space_gauge, stat_item_areas[i + 3]); f.render_widget(space_gauge, stat_item_areas[i + 3]);
} }
f.render_widget(folders, stat_item_areas[disk_space_vec.len() + 3]); f.render_widget(folders, stat_item_areas[monitored_disk_space_vec.len() + 3]);
for i in 0..root_folders.items.len() { for i in 0..monitored_root_folders.len() {
let RootFolder { let RootFolder {
path, free_space, .. path, free_space, ..
} = &root_folders.items[i]; } = &monitored_root_folders[i];
let space: f64 = convert_to_gb(*free_space); let space: f64 = convert_to_gb(*free_space);
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free"))
.block(borderless_block()) .block(borderless_block())
@@ -183,7 +184,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget( f.render_widget(
root_folder_space, root_folder_space,
stat_item_areas[i + disk_space_vec.len() + 4], stat_item_areas[i + monitored_disk_space_vec.len() + 4],
) )
} }
} else { } else {
+69
View File
@@ -2,6 +2,7 @@
mod snapshot_tests { mod snapshot_tests {
use crate::app::App; use crate::app::App;
use crate::handlers::populate_keymapping_table; use crate::handlers::populate_keymapping_table;
use crate::models::servarr_data::Notification;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
@@ -46,6 +47,40 @@ mod snapshot_tests {
insta::assert_snapshot!(output); insta::assert_snapshot!(output);
} }
#[test]
fn test_radarr_ui_renders_notification_success_popup() {
let mut app = App::test_default_fully_populated();
app.notification = Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
));
app.push_navigation_stack(ActiveRadarrBlock::default().into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ui(f, app);
});
insta::assert_snapshot!(output);
}
#[test]
fn test_radarr_ui_renders_notification_failure_popup() {
let mut app = App::test_default_fully_populated();
app.notification = Some(Notification::new(
"Download Failed".to_owned(),
"Request failed. Received 500 response code".to_owned(),
false,
));
app.push_navigation_stack(ActiveRadarrBlock::default().into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ui(f, app);
});
insta::assert_snapshot!(output);
}
#[test] #[test]
fn test_sonarr_ui_renders_library_tab() { fn test_sonarr_ui_renders_library_tab() {
let mut app = App::test_default_fully_populated(); let mut app = App::test_default_fully_populated();
@@ -84,6 +119,23 @@ mod snapshot_tests {
insta::assert_snapshot!(output); insta::assert_snapshot!(output);
} }
#[test]
fn test_sonarr_ui_renders_notification_success_popup() {
let mut app = App::test_default_fully_populated();
app.notification = Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
));
app.push_navigation_stack(ActiveSonarrBlock::default().into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ui(f, app);
});
insta::assert_snapshot!(output);
}
#[test] #[test]
fn test_lidarr_ui_renders_library_tab() { fn test_lidarr_ui_renders_library_tab() {
let mut app = App::test_default_fully_populated(); let mut app = App::test_default_fully_populated();
@@ -109,6 +161,23 @@ mod snapshot_tests {
insta::assert_snapshot!(output); insta::assert_snapshot!(output);
} }
#[test]
fn test_lidarr_ui_renders_notification_success_popup() {
let mut app = App::test_default_fully_populated();
app.notification = Some(Notification::new(
"Download Result".to_owned(),
"Download request sent successfully".to_owned(),
true,
));
app.push_navigation_stack(ActiveLidarrBlock::default().into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
ui(f, app);
});
insta::assert_snapshot!(output);
}
#[test] #[test]
fn test_lidarr_ui_renders_library_tab_error_popup() { fn test_lidarr_ui_renders_library_tab_error_popup() {
let mut app = App::test_default_fully_populated(); let mut app = App::test_default_fully_populated();
+121
View File
@@ -1,3 +1,5 @@
use crate::app::App;
use crate::models::servarr_models::{DiskSpace, RootFolder};
use crate::ui::THEME; use crate::ui::THEME;
use crate::ui::styles::{ use crate::ui::styles::{
ManagarrStyle, default_style, failure_style, primary_style, secondary_style, ManagarrStyle, default_style, failure_style, primary_style, secondary_style,
@@ -7,6 +9,8 @@ use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text}; use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap}; use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[cfg(test)] #[cfg(test)]
#[path = "utils_tests.rs"] #[path = "utils_tests.rs"]
@@ -179,3 +183,120 @@ pub(super) fn decorate_peer_style(seeders: u64, leechers: u64, text: Text<'_>) -
text.success() text.success()
} }
} }
pub(super) fn extract_monitored_root_folders(
app: &App<'_>,
root_folders: Vec<RootFolder>,
) -> Vec<RootFolder> {
let monitored_paths = app
.server_tabs
.get_active_config()
.as_ref()
.unwrap()
.monitored_storage_paths
.as_ref();
if let Some(monitored_paths) = monitored_paths
&& !monitored_paths.is_empty()
{
let monitored_paths: Vec<PathBuf> = monitored_paths.iter().map(PathBuf::from).collect();
let mut collapsed_folders: HashMap<PathBuf, (RootFolder, Vec<String>)> = HashMap::new();
let mut unmatched_folders: Vec<RootFolder> = Vec::new();
for root_folder in root_folders {
let root_path = Path::new(&root_folder.path);
let matching_monitored_path = monitored_paths
.iter()
.filter(|mp| root_path.starts_with(mp))
.max_by_key(|mp| mp.components().count());
if let Some(monitored_path) = matching_monitored_path {
let subfolder_name = root_path
.strip_prefix(monitored_path)
.ok()
.and_then(|p| p.components().next())
.map(|c| c.as_os_str().to_string_lossy().to_string())
.unwrap_or_default();
collapsed_folders
.entry(monitored_path.clone())
.and_modify(|(_, subfolders)| {
if !subfolder_name.is_empty() && !subfolders.contains(&subfolder_name) {
subfolders.push(subfolder_name.clone());
}
})
.or_insert_with(|| {
let subfolders = if subfolder_name.is_empty() {
vec![]
} else {
vec![subfolder_name]
};
(root_folder.clone(), subfolders)
});
} else {
unmatched_folders.push(root_folder);
}
}
let mut result: Vec<RootFolder> = collapsed_folders
.into_iter()
.map(|(monitored_path, (mut root_folder, mut subfolders))| {
subfolders.sort();
let path_str = monitored_path.to_string_lossy();
root_folder.path = if subfolders.is_empty() {
path_str.to_string()
} else {
format!(
"{}/[{}]",
path_str.trim_end_matches('/'),
subfolders.join(",")
)
};
root_folder
})
.collect();
result.extend(unmatched_folders);
result.sort_by(|a, b| a.path.cmp(&b.path));
result
} else {
root_folders
}
}
pub(super) fn extract_monitored_disk_space_vec(
app: &App<'_>,
disk_space_vec: Vec<DiskSpace>,
) -> Vec<DiskSpace> {
let monitored_paths = app
.server_tabs
.get_active_config()
.as_ref()
.unwrap()
.monitored_storage_paths
.as_ref();
if let Some(monitored_paths) = monitored_paths
&& !monitored_paths.is_empty()
{
let monitored: HashSet<&str> = monitored_paths.iter().map(|s| s.as_str()).collect();
let mut seen_paths = HashSet::new();
let mut filtered_disk_space_vec = Vec::with_capacity(disk_space_vec.len());
for ds in disk_space_vec {
match ds.path.as_deref() {
None => filtered_disk_space_vec.push(ds),
Some(p) => {
if monitored.contains(p) && seen_paths.insert(p.to_owned()) {
filtered_disk_space_vec.push(ds)
}
}
}
}
filtered_disk_space_vec
} else {
disk_space_vec
}
}
+285 -1
View File
@@ -1,9 +1,12 @@
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::app::{App, ServarrConfig};
use crate::models::servarr_models::{DiskSpace, RootFolder};
use crate::ui::styles::{ManagarrStyle, default_style, failure_style, secondary_style}; use crate::ui::styles::{ManagarrStyle, default_style, failure_style, secondary_style};
use crate::ui::utils::{ use crate::ui::utils::{
borderless_block, centered_rect, convert_to_minutes_hours_days, decorate_peer_style, borderless_block, centered_rect, convert_to_minutes_hours_days, decorate_peer_style,
get_width_from_percentage, layout_block, layout_block_bottom_border, layout_block_top_border, extract_monitored_disk_space_vec, extract_monitored_root_folders, get_width_from_percentage,
layout_block, layout_block_bottom_border, layout_block_top_border,
layout_block_top_border_with_title, layout_block_with_title, logo_block, style_block_highlight, layout_block_top_border_with_title, layout_block_with_title, logo_block, style_block_highlight,
style_log_list_item, title_block, title_block_centered, title_style, unstyled_title_block, style_log_list_item, title_block, title_block_centered, title_style, unstyled_title_block,
}; };
@@ -278,6 +281,287 @@ mod test {
} }
} }
#[test]
fn test_extract_monitored_root_folders_collapses_subfolders() {
let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig {
monitored_storage_paths: Some(vec!["/nfs".to_owned()]),
..ServarrConfig::default()
});
let root_folders = vec![
RootFolder {
id: 1,
path: "/nfs/cartoons".to_string(),
accessible: true,
free_space: 100,
unmapped_folders: None,
},
RootFolder {
id: 2,
path: "/nfs/tv".to_string(),
accessible: true,
free_space: 100,
unmapped_folders: None,
},
RootFolder {
id: 3,
path: "/nfs/reality".to_string(),
accessible: true,
free_space: 100,
unmapped_folders: None,
},
];
let monitored_root_folders = extract_monitored_root_folders(&app, root_folders);
assert_eq!(monitored_root_folders.len(), 1);
assert_eq!(monitored_root_folders[0].path, "/nfs/[cartoons,reality,tv]");
assert_eq!(monitored_root_folders[0].free_space, 100);
}
#[test]
fn test_extract_monitored_root_folders_uses_most_specific_monitored_path() {
let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig {
monitored_storage_paths: Some(vec!["/nfs".to_owned(), "/".to_owned()]),
..ServarrConfig::default()
});
let root_folders = vec![
RootFolder {
id: 1,
path: "/nfs/cartoons".to_string(),
accessible: true,
free_space: 100,
unmapped_folders: None,
},
RootFolder {
id: 2,
path: "/nfs/tv".to_string(),
accessible: true,
free_space: 100,
unmapped_folders: None,
},
RootFolder {
id: 3,
path: "/other/movies".to_string(),
accessible: true,
free_space: 200,
unmapped_folders: None,
},
];
let monitored_root_folders = extract_monitored_root_folders(&app, root_folders);
assert_eq!(monitored_root_folders.len(), 2);
assert_eq!(monitored_root_folders[0].path, "/[other]");
assert_eq!(monitored_root_folders[0].free_space, 200);
assert_eq!(monitored_root_folders[1].path, "/nfs/[cartoons,tv]");
assert_eq!(monitored_root_folders[1].free_space, 100);
}
#[test]
fn test_extract_monitored_root_folders_preserves_unmatched_folders() {
let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig {
monitored_storage_paths: Some(vec!["/nfs".to_owned()]),
..ServarrConfig::default()
});
let root_folders = vec![
RootFolder {
id: 1,
path: "/nfs/tv".to_string(),
accessible: true,
free_space: 100,
unmapped_folders: None,
},
RootFolder {
id: 2,
path: "/other/movies".to_string(),
accessible: true,
free_space: 200,
unmapped_folders: None,
},
];
let monitored_root_folders = extract_monitored_root_folders(&app, root_folders);
assert_eq!(monitored_root_folders.len(), 2);
assert_eq!(monitored_root_folders[0].path, "/nfs/[tv]");
assert_eq!(monitored_root_folders[1].path, "/other/movies");
}
#[test]
fn test_extract_monitored_root_folders_returns_all_when_monitored_storage_paths_is_empty() {
let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig {
monitored_storage_paths: Some(vec![]),
..ServarrConfig::default()
});
let root_folders = vec![
RootFolder {
id: 1,
path: "/nfs".to_string(),
accessible: true,
free_space: 10,
unmapped_folders: None,
},
RootFolder {
id: 2,
path: "/nfs/some/subpath".to_string(),
accessible: true,
free_space: 10,
unmapped_folders: None,
},
];
let monitored_root_folders = extract_monitored_root_folders(&app, root_folders.clone());
assert_eq!(monitored_root_folders, root_folders);
}
#[test]
fn test_extract_monitored_root_folders_returns_all_when_monitored_storage_paths_is_none() {
let app = App::test_default();
let root_folders = vec![
RootFolder {
id: 1,
path: "/nfs".to_string(),
accessible: true,
free_space: 10,
unmapped_folders: None,
},
RootFolder {
id: 2,
path: "/nfs/some/subpath".to_string(),
accessible: true,
free_space: 10,
unmapped_folders: None,
},
];
let monitored_root_folders = extract_monitored_root_folders(&app, root_folders.clone());
assert_eq!(monitored_root_folders, root_folders);
}
#[test]
fn test_extract_monitored_root_folders_exact_match_shows_no_brackets() {
let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig {
monitored_storage_paths: Some(vec!["/nfs/tv".to_owned()]),
..ServarrConfig::default()
});
let root_folders = vec![RootFolder {
id: 1,
path: "/nfs/tv".to_string(),
accessible: true,
free_space: 100,
unmapped_folders: None,
}];
let monitored_root_folders = extract_monitored_root_folders(&app, root_folders);
assert_eq!(monitored_root_folders.len(), 1);
assert_eq!(monitored_root_folders[0].path, "/nfs/tv");
}
#[test]
fn test_extract_monitored_disk_space_vec() {
let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig {
monitored_storage_paths: Some(vec!["/data".to_owned(), "/downloads".to_owned()]),
..ServarrConfig::default()
});
let disk_space = DiskSpace {
path: Some("/data".to_string()),
free_space: 10,
total_space: 1000,
};
let disk_space_2 = DiskSpace {
path: Some("/downloads".to_string()),
free_space: 100,
total_space: 10000,
};
let disk_space_with_empty_path = DiskSpace {
path: None,
free_space: 10,
total_space: 1000,
};
let disk_spaces = vec![
disk_space.clone(),
disk_space_with_empty_path.clone(),
DiskSpace {
path: Some("/downloads/".to_string()),
free_space: 100,
total_space: 10000,
},
disk_space_2.clone(),
];
let monitored_disk_space = extract_monitored_disk_space_vec(&app, disk_spaces);
assert_eq!(
monitored_disk_space,
vec![disk_space, disk_space_with_empty_path, disk_space_2]
);
}
#[test]
fn test_extract_monitored_disk_space_vec_returns_all_when_monitored_storage_paths_is_empty() {
let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig {
monitored_storage_paths: Some(Vec::new()),
..ServarrConfig::default()
});
let disk_spaces = vec![
DiskSpace {
path: Some("/nfs".to_string()),
free_space: 10,
total_space: 1000,
},
DiskSpace {
path: None,
free_space: 10,
total_space: 1000,
},
DiskSpace {
path: Some("/nfs/some/subpath".to_string()),
free_space: 10,
total_space: 1000,
},
];
let monitored_disk_space = extract_monitored_disk_space_vec(&app, disk_spaces.clone());
assert_eq!(monitored_disk_space, disk_spaces);
}
#[test]
fn test_extract_monitored_disk_space_vec_returns_all_when_monitored_storage_paths_is_none() {
let app = App::test_default();
let disk_spaces = vec![
DiskSpace {
path: Some("/nfs".to_string()),
free_space: 10,
total_space: 1000,
},
DiskSpace {
path: None,
free_space: 10,
total_space: 1000,
},
DiskSpace {
path: Some("/nfs/some/subpath".to_string()),
free_space: 10,
total_space: 1000,
},
];
let monitored_disk_space = extract_monitored_disk_space_vec(&app, disk_spaces.clone());
assert_eq!(monitored_disk_space, disk_spaces);
}
enum PeerStyle { enum PeerStyle {
Failure, Failure,
Warning, Warning,
+1 -1
View File
@@ -146,7 +146,7 @@ pub(super) fn load_config(path: &str) -> Result<AppConfig> {
Ok(config) Ok(config)
} }
Err(e) => { Err(e) => {
log_and_print_error(format!("Unable to open config file: {e:?}")); log_and_print_error(format!("Unable to open config file '{path}': {e:?}"));
process::exit(1); process::exit(1);
} }
} }