diff --git a/Cargo.lock b/Cargo.lock index 02fa3e7..0cb1525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -305,9 +305,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.53" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -341,9 +341,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" dependencies = [ "clap_builder", "clap_derive", @@ -351,9 +351,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" dependencies = [ "anstream", "anstyle", @@ -373,9 +373,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -420,14 +420,15 @@ dependencies = [ [[package]] name = "confy" -version = "0.6.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0" +checksum = "8807c397789cbe02bbdb1a27ea5f345584132808697b2a3f957c829829ee4814" dependencies = [ - "directories", + "etcetera", + "lazy_static", "serde", "serde_yaml", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -561,6 +562,16 @@ dependencies = [ "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]] name = "darling" version = "0.23.0" @@ -585,6 +596,20 @@ dependencies = [ "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]] name = "darling_core" version = "0.23.0" @@ -609,6 +634,17 @@ dependencies = [ "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]] name = "darling_macro" version = "0.23.0" @@ -671,11 +707,11 @@ dependencies = [ [[package]] name = "derive_setters" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" +checksum = "b7e6f6fa1f03c14ae082120b84b3c7fbd7b8588d924cf2d7c3daf9afd49df8b9" dependencies = [ - "darling 0.20.11", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.114", @@ -715,15 +751,6 @@ dependencies = [ "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]] name = "dirs-next" version = "2.0.0" @@ -734,18 +761,6 @@ dependencies = [ "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]] name = "dirs-sys-next" version = "0.1.2" @@ -852,6 +867,17 @@ dependencies = [ "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]] name = "euclid" version = "0.22.13" @@ -890,9 +916,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "finl_unicode" @@ -1104,6 +1130,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "http" version = "1.4.0" @@ -1254,9 +1289,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1869,9 +1904,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -1997,9 +2032,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] 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" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" dependencies = [ "cc", ] @@ -2017,12 +2052,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "ordered-float" version = "2.10.1" @@ -2261,9 +2290,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2295,9 +2324,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -2911,9 +2940,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" @@ -2929,9 +2958,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -3225,9 +3254,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "libc", @@ -3240,9 +3269,9 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "tinystr" @@ -3548,9 +3577,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "atomic", "getrandom 0.3.4", @@ -3975,15 +4004,6 @@ dependencies = [ "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]] name = "windows-sys" version = "0.52.0" @@ -4020,21 +4040,6 @@ dependencies = [ "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]] name = "windows-targets" version = "0.52.6" @@ -4077,12 +4082,6 @@ dependencies = [ "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]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4095,12 +4094,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4113,12 +4106,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4143,12 +4130,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4161,12 +4142,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4179,12 +4154,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4197,12 +4166,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4267,18 +4230,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" dependencies = [ "proc-macro2", "quote", @@ -4347,6 +4310,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/Cargo.toml b/Cargo.toml index ea10f9e..473643c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,67 +20,67 @@ members = [ ] [dependencies] -anyhow = "1.0.68" -backtrace = "0.3.74" +anyhow = "1.0.100" +backtrace = "0.3.76" bimap = { version = "0.6.3", features = ["serde"] } -chrono = { version = "0.4.38", features = ["serde"] } -confy = { version = "0.6.0", default-features = false, features = [ +chrono = { version = "0.4.43", features = ["serde"] } +confy = { version = "2.0.0", default-features = false, features = [ "yaml_conf", ] } crossterm = "0.28.1" derivative = "2.2.0" -human-panic = "2.0.2" -indoc = "2.0.0" -log = "0.4.17" -log4rs = { version = "1.2.0", features = ["file_appender"] } -regex = "1.11.1" -reqwest = { version = "0.12.9", features = ["json"] } -serde_yaml = "0.9.16" -serde_json = "1.0.91" -serde = { version = "1.0.214", features = ["derive"] } +human-panic = "2.0.6" +indoc = "2.0.7" +log = "0.4.29" +log4rs = { version = "1.4.0", features = ["file_appender"] } +regex = "1.12.2" +reqwest = { version = "0.12.28", features = ["json"] } +serde_yaml = "0.9.34" +serde_json = "1.0.149" +serde = { version = "1.0.228", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] } strum_macros = "0.26.4" -tokio = { version = "1.44.2", features = ["full"] } -tokio-util = "0.7.8" +tokio = { version = "1.49.0", features = ["full"] } +tokio-util = "0.7.18" ratatui = { version = "0.30.0", features = [ "all-widgets", "unstable-widget-ref", ] } -urlencoding = "2.1.2" -clap = { version = "4.5.20", features = [ +urlencoding = "2.1.3" +clap = { version = "4.5.56", features = [ "derive", "cargo", "env", "wrap_help", ] } -clap_complete = "4.5.33" +clap_complete = "4.5.65" itertools = "0.14.0" -ctrlc = "3.4.5" -colored = "3.0.0" -async-trait = "0.1.83" +ctrlc = "3.5.1" +colored = "3.1.1" +async-trait = "0.1.89" dirs-next = "2.0.0" managarr-tree-widget = "0.25.0" -indicatif = "0.17.9" -derive_setters = "0.1.6" -deunicode = "1.6.0" -openssl = { version = "0.10.70", features = ["vendored"] } +indicatif = "0.17.11" +derive_setters = "0.1.9" +deunicode = "1.6.2" +openssl = { version = "0.10.75", features = ["vendored"] } veil = "0.2.0" validate_theme_derive = "0.1.0" enum_display_style_derive = "0.1.0" [dev-dependencies] -assert_cmd = "2.0.16" -mockall = "0.13.0" -mockito = "1.0.0" -pretty_assertions = "1.3.0" -proptest = "1.6.0" +assert_cmd = "2.1.2" +mockall = "0.13.1" +mockito = "1.7.1" +pretty_assertions = "1.4.1" +proptest = "1.9.0" rstest = "0.25.0" -serial_test = "3.2.0" -assertables = "9.8.2" -insta = "1.41.1" +serial_test = "3.3.1" +assertables = "9.8.4" +insta = "1.46.1" [dev-dependencies.cargo-husky] -version = "1" +version = "1.5.0" default-features = false features = ["user-hooks"] diff --git a/README.md b/README.md index 61b1c65..07cc4e2 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,8 @@ Commands: lidarr Commands for manging your Lidarr instance completions Generate shell completions for the Managarr CLI tail-logs Tail Managarr logs + config-path Print the full path to the default configuration file. + This file can be changed to another location using the '--config-file' flag help Print this message or the help of the given subcommand(s) Options: @@ -266,14 +268,23 @@ Options: -V, --version Print version Global Options: - --disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=] - --config-file The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=] - --themes-file The Managarr themes file to use [env: MANAGARR_THEMES_FILE=] - --theme The name of the Managarr theme to use [env: MANAGARR_THEME=] - --servarr-name For multi-instance configurations, you need to specify the name of the instance configuration that you want to use. + --disable-spinner Disable the spinner (can sometimes make parsing output + challenging) [env: MANAGARR_DISABLE_SPINNER=] + --config-file The Managarr configuration file to use; defaults to the + path shown by 'managarr config-path' [env: + MANAGARR_CONFIG_FILE=] + --themes-file The Managarr themes file to use [env: + MANAGARR_THEMES_FILE=] + --theme The name of the Managarr theme to use [env: + MANAGARR_THEME=] + --servarr-name For multi-instance configurations, you need to specify + the name of the instance configuration that you want to + use. - This is useful when you have multiple instances of the same Servarr defined in your config file. - By default, if left empty, the first configured Servarr instance listed in the config file will be used. + This is useful when you have multiple instances of the + same Servarr defined in your config file. + By default, if left empty, the first configured Servarr + instance listed in the config file will be used. ``` All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run: @@ -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), 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 -``` -$HOME/.config/managarr/config.yml -``` - -### Mac -``` -$HOME/Library/Application Support/managarr/config.yml -``` - -### Windows -``` -%APPDATA%/Roaming/managarr/config.yml +```shell +managarr config-path ``` ## Specify Which Configuration File to Use @@ -364,42 +365,39 @@ radarr: port: 7878 api_token: someApiToken1234567890 ssl_cert_path: /path/to/radarr.crt # Required to enable SSL -sonarr: - - uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port' - api_token: someApiToken1234567890 + - uri: http://htpc.local/radarr # Example of using the 'uri' key instead of 'host' and 'port' + api_token: someApiToken1234567890 + +sonarr: + - host: 192.168.0.89 + port: 8989 + api_token_file: /root/.config/sonarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file + - name: Anime Sonarr # An example of a custom name for a secondary Sonarr instance - host: 192.168.0.89 + host: 192.168.1.89 port: 8989 api_token: someApiToken1234567890 -readarr: - - host: 192.168.0.87 - port: 8787 - api_token_file: /root/.config/readarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file + lidarr: - host: 192.168.0.86 port: 8686 api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables -whisparr: - - host: 192.168.0.69 - port: 6969 + monitored_storage_paths: # Filter which Root Folders or Disk Storage you want displayed in the UI's 'Stats' block + # Note: Setting these values does not affect what shows up in the 'Root Folders' tab of the UI. + - /nfs # An example disk (i.e. ' list disk-space' command) you want displayed in the UI under 'Storage:' + - /media # An example root folder you want displayed in the UI + # Root folders collapse up to the super-directory to reduce duplication in the UI. For example: + # if you have root folders '/media/tv', '/media/cartoons' and '/media/reality', and you set this + # monitored path, the UI will show '/media/[tv,cartoons,reality]' under Root Folders + + - host: 192.168.1.86 + port: 8686 api_token: someApiToken1234567890 - ssl_cert_path: /path/to/whisparr.crt + ssl_cert_path: /path/to/lidarr_1.crt custom_headers: # Example of adding custom headers to all requests to the Servarr instance traefik-auth-bypass-key: someBypassKey1234567890 SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE} -bazarr: - - host: 192.168.0.67 - port: 6767 - api_token: someApiToken1234567890 -prowlarr: - - host: 192.168.0.96 - port: 9696 - api_token: someApiToken1234567890 -tautulli: - - host: 192.168.0.81 - port: 8181 - api_token: someApiToken1234567890 ``` ### Example Multi-Instance Configuration: diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index c47bcf5..8b8201e 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -507,6 +507,56 @@ mod tests { assert_none!(config.custom_headers); } + #[test] + #[serial] + fn test_deserialize_optional_env_var_string_vec_is_present() { + unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION", "/path1") }; + let expected_monitored_paths = ["/path1", "/path2"]; + let yaml_data = r#" + monitored_storage_paths: + - ${TEST_VAR_DESERIALIZE_STRING_VEC_OPTION} + - /path2 + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths); + unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION") }; + } + + #[test] + #[serial] + fn test_deserialize_optional_env_var_string_vec_does_not_overwrite_non_env_value() { + unsafe { + std::env::set_var( + "TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE", + "/path3", + ) + }; + let expected_monitored_paths = ["/path1", "/path2"]; + let yaml_data = r#" + monitored_storage_paths: + - /path1 + - /path2 + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths); + unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE") }; + } + + #[test] + fn test_deserialize_optional_env_var_string_vec_empty() { + let yaml_data = r#" + api_token: "test123" + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_none!(config.monitored_storage_paths); + } + #[test] #[serial] fn test_deserialize_optional_u16_env_var_is_present() { @@ -620,10 +670,11 @@ mod tests { let api_token = "thisisatest".to_owned(); let api_token_file = "/root/.config/api_token".to_owned(); let ssl_cert_path = "/some/path".to_owned(); + let monitored_storage = vec!["/path1".to_owned(), "/path2".to_owned()]; let mut custom_headers = HeaderMap::new(); custom_headers.insert("X-Custom-Header", "value".parse().unwrap()); let expected_str = format!( - "ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}) }}" + "ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}" ); let servarr_config = ServarrConfig { name: Some(name), @@ -635,6 +686,7 @@ mod tests { api_token_file: Some(api_token_file), ssl_cert_path: Some(ssl_cert_path), custom_headers: Some(custom_headers), + monitored_storage_paths: Some(monitored_storage), }; assert_str_eq!(format!("{servarr_config:?}"), expected_str); diff --git a/src/app/mod.rs b/src/app/mod.rs index 9524980..2ab870d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken; use veil::Redact; use crate::cli::Command; +use crate::models::servarr_data::Notification; use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; @@ -38,6 +39,7 @@ pub struct App<'a> { pub server_tabs: TabState, pub keymapping_table: Option>, pub error: HorizontallyScrollableText, + pub notification: Option, pub tick_until_poll: u64, pub ticks_until_scroll: u64, pub tick_count: u64, @@ -254,6 +256,7 @@ impl Default for App<'_> { cancellation_token: CancellationToken::new(), keymapping_table: None, error: HorizontallyScrollableText::default(), + notification: None, is_first_render: true, server_tabs: TabState::new(Vec::new()), tick_until_poll: 400, @@ -346,11 +349,11 @@ pub struct 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() { - log_and_print_error( - "No Servarr configuration provided in the specified configuration file".to_owned(), - ); + log_and_print_error(format!( + "No Servarrs are configured in the file: {config_path}" + )); process::exit(1); } @@ -436,6 +439,8 @@ pub struct ServarrConfig { serialize_with = "serialize_header_map" )] pub custom_headers: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var_string_vec")] + pub monitored_storage_paths: Option>, } impl ServarrConfig { @@ -482,6 +487,7 @@ impl Default for ServarrConfig { api_token_file: None, ssl_cert_path: None, custom_headers: None, + monitored_storage_paths: None, } } } @@ -548,6 +554,24 @@ where } } +fn deserialize_optional_env_var_string_vec<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let opt: Option> = 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, D::Error> where D: serde::Deserializer<'de>, diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs index 36109fd..e09658f 100644 --- a/src/cli/lidarr/list_command_handler.rs +++ b/src/cli/lidarr/list_command_handler.rs @@ -59,7 +59,7 @@ pub enum LidarrListCommand { Artists, #[command(about = "List all items in the Lidarr 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, #[command(about = "List all active downloads in Lidarr")] Downloads { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 872a18e..bb5acd1 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Result; use clap::{Subcommand, command}; use clap_complete::Shell; +use indoc::indoc; use lidarr::{LidarrCliHandler, LidarrCommand}; use radarr::{RadarrCliHandler, RadarrCommand}; use sonarr::{SonarrCliHandler, SonarrCommand}; @@ -43,6 +44,12 @@ pub enum Command { #[arg(long, help = "Disable colored log output")] no_color: bool, }, + + #[command(about = indoc!{" + Print the full path to the default configuration file. + This file can be changed to another location using the '--config-file' flag + "})] + ConfigPath, } pub trait CliCommandHandler<'a, 'b, T: Into> { diff --git a/src/handlers/handlers_tests.rs b/src/handlers/handlers_tests.rs index 819921b..c436db9 100644 --- a/src/handlers/handlers_tests.rs +++ b/src/handlers/handlers_tests.rs @@ -20,10 +20,10 @@ mod tests { use crate::handlers::{handle_events, populate_keymapping_table}; use crate::models::HorizontallyScrollableText; use crate::models::Route; - use crate::models::servarr_data::ActiveKeybindingBlock; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; 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::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] fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) { 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 { let (key, alt_key) = if key.alt.is_some() { (key.key.to_string(), key.alt.as_ref().unwrap().to_string()) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 316ba13..375959c 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -116,6 +116,8 @@ pub fn handle_events(key: Key, app: &mut App<'_>) { } else { app.keymapping_table = None; } + } else if matches_key!(esc, key) && app.notification.is_some() { + app.notification.take(); } else { match app.get_current_route() { _ if app.keymapping_table.is_some() => { diff --git a/src/logos.rs b/src/logos.rs index f0c6815..0205aff 100644 --- a/src/logos.rs +++ b/src/logos.rs @@ -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 = "⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ ⠀⢠⣾⠟⠋⠀⠀⢀⣀⠀⠙⠻⣷⡄⠀ ⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄ diff --git a/src/main.rs b/src/main.rs index 4baa095..033c440 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ #[macro_use] extern crate assertables; -use anyhow::Result; +use anyhow::{Context, Result}; use clap::{ Args, CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version, }; @@ -86,7 +86,7 @@ struct GlobalOpts { global = true, value_parser, 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, #[arg( @@ -127,15 +127,30 @@ async fn main() -> Result<()> { let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); let args = Cli::parse(); - let mut config = if let Some(ref config_file) = args.global.config_file { - load_config(config_file.to_str().expect("Invalid config file specified"))? + let config_file_path = confy::get_configuration_file_path("managarr", "config")?; + 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 { - 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 spinner_disabled = args.global.disable_spinner; debug!("Managarr loaded using config: {config:?}"); - config.validate(); + config.validate(&config_path); config.post_process_initialization(); let reqwest_client = build_network_client(&config); @@ -170,6 +185,11 @@ async fn main() -> Result<()> { generate(shell, &mut cli, "managarr", &mut io::stdout()) } 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 => { let app_nw = Arc::clone(&app); diff --git a/src/models/servarr_data/mod.rs b/src/models/servarr_data/mod.rs index 256f0bc..c058380 100644 --- a/src/models/servarr_data/mod.rs +++ b/src/models/servarr_data/mod.rs @@ -10,6 +10,23 @@ pub(in crate::models::servarr_data) mod data_test_utils; #[cfg(test)] 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)] pub enum ActiveKeybindingBlock { #[default] diff --git a/src/network/lidarr_network/library/lidarr_library_network_tests.rs b/src/network/lidarr_network/library/lidarr_library_network_tests.rs index 133d37b..333d8a6 100644 --- a/src/network/lidarr_network/library/lidarr_library_network_tests.rs +++ b/src/network/lidarr_network/library/lidarr_library_network_tests.rs @@ -1,8 +1,10 @@ #[cfg(test)] mod tests { use crate::models::lidarr_models::LidarrReleaseDownloadBody; + use crate::models::servarr_data::Notification; use crate::network::lidarr_network::LidarrEvent; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use pretty_assertions::assert_eq; use serde_json::json; #[tokio::test] @@ -30,5 +32,51 @@ mod tests { mock.assert_async().await; 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, + ) + ); } } diff --git a/src/network/lidarr_network/library/mod.rs b/src/network/lidarr_network/library/mod.rs index f20624f..aa68d60 100644 --- a/src/network/lidarr_network/library/mod.rs +++ b/src/network/lidarr_network/library/mod.rs @@ -1,4 +1,5 @@ use crate::models::lidarr_models::LidarrReleaseDownloadBody; +use crate::models::servarr_data::Notification; use crate::network::lidarr_network::LidarrEvent; use crate::network::{Network, RequestMethod}; use anyhow::Result; @@ -31,8 +32,26 @@ impl Network<'_, '_> { ) .await; - self - .handle_request::(request_props, |_, _| ()) - .await + let result = self + .handle_request::(request_props, |_, mut app| { + 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 } } diff --git a/src/network/lidarr_network/system/lidarr_system_network_tests.rs b/src/network/lidarr_network/system/lidarr_system_network_tests.rs index ab97ab8..596df7b 100644 --- a/src/network/lidarr_network/system/lidarr_system_network_tests.rs +++ b/src/network/lidarr_network/system/lidarr_system_network_tests.rs @@ -16,21 +16,34 @@ mod tests { async fn test_handle_get_diskspace_event() { let diskspace_json = json!([ { + "path": "/path1", "freeSpace": 1111, "totalSpace": 2222, }, { + "path": "/path2", "freeSpace": 3333, "totalSpace": 4444 } ]); - let response: Vec = serde_json::from_value(diskspace_json.clone()).unwrap(); let (mock, app, _server) = MockServarrApi::get() .returns(diskspace_json) .build_for(LidarrEvent::GetDiskSpace) .await; app.lock().await.server_tabs.set_index(2); 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; @@ -40,8 +53,11 @@ mod tests { panic!("Expected DiskSpaces"); }; - assert_eq!(disk_spaces, response); - assert!(!app.lock().await.data.lidarr_data.disk_space_vec.is_empty()); + assert_eq!( + app.lock().await.data.lidarr_data.disk_space_vec, + disk_space_vec + ); + assert_eq!(disk_spaces, disk_space_vec); } #[tokio::test] diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index 871e9d6..cbf7073 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -862,6 +862,7 @@ pub(in crate::network) mod test_utils { host, port, api_token: Some("test1234".to_owned()), + monitored_storage_paths: Some(vec!["/path1".to_owned()]), ..ServarrConfig::default() }; diff --git a/src/network/radarr_network/library/mod.rs b/src/network/radarr_network/library/mod.rs index c68a63c..42b2d67 100644 --- a/src/network/radarr_network/library/mod.rs +++ b/src/network/radarr_network/library/mod.rs @@ -3,6 +3,7 @@ use crate::models::radarr_models::{ EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody, }; +use crate::models::servarr_data::Notification; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::stateful_table::StatefulTable; @@ -85,9 +86,27 @@ impl Network<'_, '_> { .request_props_from(event, RequestMethod::Post, Some(params), None, None) .await; - self - .handle_request::(request_props, |_, _| ()) - .await + let result = self + .handle_request::(request_props, |_, mut app| { + 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( diff --git a/src/network/radarr_network/library/radarr_library_network_tests.rs b/src/network/radarr_network/library/radarr_library_network_tests.rs index 26215e4..941f010 100644 --- a/src/network/radarr_network/library/radarr_library_network_tests.rs +++ b/src/network/radarr_network/library/radarr_library_network_tests.rs @@ -4,6 +4,7 @@ mod tests { AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, EditMovieParams, 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::radarr_data::ActiveRadarrBlock; use crate::models::stateful_table::SortOption; @@ -164,14 +165,58 @@ mod tests { .await; let mut network = test_network(&app); - assert!( - network - .handle_radarr_event(RadarrEvent::DownloadRelease(expected_body)) - .await - .is_ok() - ); + let result = network + .handle_radarr_event(RadarrEvent::DownloadRelease(expected_body)) + .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] diff --git a/src/network/sonarr_network/library/mod.rs b/src/network/sonarr_network/library/mod.rs index 15c4a2c..b09c301 100644 --- a/src/network/sonarr_network/library/mod.rs +++ b/src/network/sonarr_network/library/mod.rs @@ -1,3 +1,4 @@ +use crate::models::servarr_data::Notification; use crate::models::sonarr_models::SonarrReleaseDownloadBody; use crate::network::sonarr_network::SonarrEvent; use crate::network::{Network, RequestMethod}; @@ -31,8 +32,26 @@ impl Network<'_, '_> { ) .await; - self - .handle_request::(request_props, |_, _| ()) - .await + let result = self + .handle_request::(request_props, |_, mut app| { + 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 } } diff --git a/src/network/sonarr_network/library/sonarr_library_network_tests.rs b/src/network/sonarr_network/library/sonarr_library_network_tests.rs index 68562d3..f777c65 100644 --- a/src/network/sonarr_network/library/sonarr_library_network_tests.rs +++ b/src/network/sonarr_network/library/sonarr_library_network_tests.rs @@ -1,8 +1,10 @@ #[cfg(test)] mod tests { + use crate::models::servarr_data::Notification; use crate::models::sonarr_models::SonarrReleaseDownloadBody; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::sonarr_network::SonarrEvent; + use pretty_assertions::assert_eq; use serde_json::json; #[tokio::test] @@ -33,5 +35,54 @@ mod tests { mock.assert_async().await; 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, + ) + ); } } diff --git a/src/ui/lidarr_ui/library/album_details_ui.rs b/src/ui/lidarr_ui/library/album_details_ui.rs index 58f6ee6..964ee8c 100644 --- a/src/ui/lidarr_ui/library/album_details_ui.rs +++ b/src/ui/lidarr_ui/library/album_details_ui.rs @@ -356,12 +356,12 @@ fn draw_album_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .clone() .unwrap_or(Number::from(0u64)) .as_u64() - .unwrap(); + .unwrap_or_default(); let leechers = leechers .clone() .unwrap_or(Number::from(0u64)) .as_u64() - .unwrap(); + .unwrap_or_default(); decorate_peer_style( seeders, diff --git a/src/ui/lidarr_ui/library/artist_details_ui.rs b/src/ui/lidarr_ui/library/artist_details_ui.rs index 9a7d381..939fea4 100644 --- a/src/ui/lidarr_ui/library/artist_details_ui.rs +++ b/src/ui/lidarr_ui/library/artist_details_ui.rs @@ -501,12 +501,12 @@ fn draw_artist_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .clone() .unwrap_or(Number::from(0u64)) .as_u64() - .unwrap(); + .unwrap_or_default(); let leechers = leechers .clone() .unwrap_or(Number::from(0u64)) .as_u64() - .unwrap(); + .unwrap_or_default(); decorate_peer_style( seeders, diff --git a/src/ui/lidarr_ui/mod.rs b/src/ui/lidarr_ui/mod.rs index 4ef8300..dbf277d 100644 --- a/src/ui/lidarr_ui/mod.rs +++ b/src/ui/lidarr_ui/mod.rs @@ -1,5 +1,3 @@ -use std::{cmp, iter}; - #[cfg(test)] use crate::ui::ui_test_utils::test_utils::Utc; use chrono::Duration; @@ -14,6 +12,7 @@ use ratatui::{ text::Text, widgets::Paragraph, }; +use std::{cmp, iter}; use super::{ 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::root_folders::RootFoldersUi; use crate::ui::lidarr_ui::system::SystemUi; +use crate::ui::utils::{extract_monitored_disk_space_vec, extract_monitored_root_folders}; use crate::{ app::App, logos::LIDARR_LOGO, @@ -100,6 +100,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { start_time, .. } = &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![ Constraint::Length(1), @@ -110,7 +112,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { constraints.append( &mut iter::repeat_n( Constraint::Length(1), - disk_space_vec.len() + root_folders.items.len() + 1, + monitored_disk_space_vec.len() + monitored_root_folders.len() + 1, ) .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(storage, stat_item_areas[2]); - for i in 0..disk_space_vec.len() { + for i in 0..monitored_disk_space_vec.len() { let DiskSpace { path, free_space, total_space, - } = &disk_space_vec[i]; + } = &monitored_disk_space_vec[i]; let title = if let Some(path) = path { path } 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(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 { path, free_space, .. - } = &root_folders.items[i]; + } = &monitored_root_folders[i]; let space: f64 = convert_to_gb(*free_space); let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) .block(borderless_block()) @@ -181,7 +183,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { f.render_widget( root_folder_space, - stat_item_areas[i + disk_space_vec.len() + 4], + stat_item_areas[i + monitored_disk_space_vec.len() + 4], ) } } else { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9842a5f..502a7c1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -14,6 +14,7 @@ use sonarr_ui::SonarrUi; use utils::layout_block; use crate::app::App; +use crate::models::servarr_data::Notification; use crate::models::servarr_models::KeybindingItem; use crate::models::{HorizontallyScrollableText, Route, TabState}; 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::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 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() { 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); } +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 { if title.is_empty() { f.render_widget(layout_block().default_color(), area); diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index ec8ae32..79561e9 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -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::logos::RADARR_LOGO; 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::system::SystemUi; use crate::ui::styles::ManagarrStyle; +#[cfg(test)] +use crate::ui::ui_test_utils::test_utils::Utc; 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::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 collections; @@ -93,6 +93,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { start_time, .. } = &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![ Constraint::Length(1), @@ -103,7 +105,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { constraints.append( &mut iter::repeat_n( Constraint::Length(1), - disk_space_vec.len() + root_folders.items.len() + 1, + monitored_disk_space_vec.len() + monitored_root_folders.len() + 1, ) .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(storage, stat_item_areas[2]); - for i in 0..disk_space_vec.len() { + for i in 0..monitored_disk_space_vec.len() { let DiskSpace { path, free_space, total_space, - } = &disk_space_vec[i]; + } = &monitored_disk_space_vec[i]; let title = if let Some(path) = path { path } 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(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 { path, free_space, .. - } = &root_folders.items[i]; + } = &monitored_root_folders[i]; let space: f64 = convert_to_gb(*free_space); let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) .block(borderless_block()) @@ -174,7 +177,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { f.render_widget( root_folder_space, - stat_item_areas[i + disk_space_vec.len() + 4], + stat_item_areas[i + monitored_disk_space_vec.len() + 4], ) } } else { diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_notification_success_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_notification_success_popup.snap new file mode 100644 index 0000000..201b8a9 --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_notification_success_popup.snap @@ -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 │ │ +│ │ │ │ +│ ╰───────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_failure_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_failure_popup.snap new file mode 100644 index 0000000..ce9f2c0 --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_failure_popup.snap @@ -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 │ │ +│ ╰───────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_success_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_success_popup.snap new file mode 100644 index 0000000..ed22e92 --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_notification_success_popup.snap @@ -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 │ │ +│ │ │ │ +│ ╰───────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_notification_success_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_notification_success_popup.snap new file mode 100644 index 0000000..7cd77bf --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_notification_success_popup.snap @@ -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 │ │ +│ │ │ │ +│ ╰───────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/sonarr_ui/library/episode_details_ui.rs b/src/ui/sonarr_ui/library/episode_details_ui.rs index 72ca7e4..3d78a26 100644 --- a/src/ui/sonarr_ui/library/episode_details_ui.rs +++ b/src/ui/sonarr_ui/library/episode_details_ui.rs @@ -425,12 +425,12 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .clone() .unwrap_or(Number::from(0u64)) .as_u64() - .unwrap(); + .unwrap_or_default(); let leechers = leechers .clone() .unwrap_or(Number::from(0u64)) .as_u64() - .unwrap(); + .unwrap_or_default(); decorate_peer_style( seeders, diff --git a/src/ui/sonarr_ui/library/season_details_ui.rs b/src/ui/sonarr_ui/library/season_details_ui.rs index 9eb28ca..f77caad 100644 --- a/src/ui/sonarr_ui/library/season_details_ui.rs +++ b/src/ui/sonarr_ui/library/season_details_ui.rs @@ -388,12 +388,12 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .clone() .unwrap_or(Number::from(0u64)) .as_u64() - .unwrap(); + .unwrap_or_default(); let leechers = leechers .clone() .unwrap_or(Number::from(0u64)) .as_u64() - .unwrap(); + .unwrap_or_default(); decorate_peer_style( seeders, diff --git a/src/ui/sonarr_ui/mod.rs b/src/ui/sonarr_ui/mod.rs index d9e081b..bb5e45c 100644 --- a/src/ui/sonarr_ui/mod.rs +++ b/src/ui/sonarr_ui/mod.rs @@ -1,5 +1,3 @@ -use std::{cmp, iter}; - #[cfg(test)] use crate::ui::ui_test_utils::test_utils::Utc; use blocklist::BlocklistUi; @@ -18,8 +16,18 @@ use ratatui::{ widgets::Paragraph, }; use root_folders::RootFoldersUi; +use std::{cmp, iter}; 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::{ app::App, logos::SONARR_LOGO, @@ -32,15 +40,6 @@ use crate::{ 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 downloads; mod history; @@ -101,6 +100,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { start_time, .. } = &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![ Constraint::Length(1), @@ -111,7 +112,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { constraints.append( &mut iter::repeat_n( Constraint::Length(1), - disk_space_vec.len() + root_folders.items.len() + 1, + monitored_disk_space_vec.len() + monitored_root_folders.len() + 1, ) .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(storage, stat_item_areas[2]); - for i in 0..disk_space_vec.len() { + for i in 0..monitored_disk_space_vec.len() { let DiskSpace { path, free_space, total_space, .. - } = &disk_space_vec[i]; + } = &monitored_disk_space_vec[i]; let title = if let Some(path) = path { path } 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(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 { path, free_space, .. - } = &root_folders.items[i]; + } = &monitored_root_folders[i]; let space: f64 = convert_to_gb(*free_space); let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) .block(borderless_block()) @@ -183,7 +184,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { f.render_widget( root_folder_space, - stat_item_areas[i + disk_space_vec.len() + 4], + stat_item_areas[i + monitored_disk_space_vec.len() + 4], ) } } else { diff --git a/src/ui/ui_tests.rs b/src/ui/ui_tests.rs index 53dfe57..555e0ba 100644 --- a/src/ui/ui_tests.rs +++ b/src/ui/ui_tests.rs @@ -2,6 +2,7 @@ mod snapshot_tests { use crate::app::App; 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::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; @@ -46,6 +47,40 @@ mod snapshot_tests { 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] fn test_sonarr_ui_renders_library_tab() { let mut app = App::test_default_fully_populated(); @@ -84,6 +119,23 @@ mod snapshot_tests { 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] fn test_lidarr_ui_renders_library_tab() { let mut app = App::test_default_fully_populated(); @@ -109,6 +161,23 @@ mod snapshot_tests { 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] fn test_lidarr_ui_renders_library_tab_error_popup() { let mut app = App::test_default_fully_populated(); diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 3fb7c3b..c349314 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,3 +1,5 @@ +use crate::app::App; +use crate::models::servarr_models::{DiskSpace, RootFolder}; use crate::ui::THEME; use crate::ui::styles::{ 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::text::{Line, Span, Text}; use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap}; +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; #[cfg(test)] #[path = "utils_tests.rs"] @@ -179,3 +183,120 @@ pub(super) fn decorate_peer_style(seeders: u64, leechers: u64, text: Text<'_>) - text.success() } } + +pub(super) fn extract_monitored_root_folders( + app: &App<'_>, + root_folders: Vec, +) -> Vec { + 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 = monitored_paths.iter().map(PathBuf::from).collect(); + + let mut collapsed_folders: HashMap)> = HashMap::new(); + let mut unmatched_folders: Vec = 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 = 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, +) -> Vec { + 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 + } +} diff --git a/src/ui/utils_tests.rs b/src/ui/utils_tests.rs index ea1f826..d8f073e 100644 --- a/src/ui/utils_tests.rs +++ b/src/ui/utils_tests.rs @@ -1,9 +1,12 @@ #[cfg(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::utils::{ 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, 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 { Failure, Warning, diff --git a/src/utils.rs b/src/utils.rs index 87e04db..7dd2e5a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -146,7 +146,7 @@ pub(super) fn load_config(path: &str) -> Result { Ok(config) } 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); } }